runtimeanalyz.ing ● early access · sign-up open

← Runtime Analysis

Sorbet and RBS make you write the types. We read them off real runs.

Ruby's types are structural: a value is whatever it can answer. Static type systems make you declare that up front and keep it in sync. Runtime Analysis reads the same contract off real runs, including the implicit interfaces your code already leans on.

The same method, typed three ways

Sorbet · Stripe
◆ enrollment.rb enrollment.rbi

1

2

3

4

class Enrollment
  extend T::Sig
  sig { returns(T::Array[Book]) }
  def unlock_next = …
Sorbetsync by hand
RBS · Ruby
◆ enrollment.rbs

1

2

3

class Enrollment
  unlock_next: () -> Array[Book]
end
RBSa file to maintain
runtime-analysis · observed
◆ enrollment.rb

1

2

3

class Enrollment
  def unlock_next = curriculum.unlocked_for(student)
    ⤷ Array<#unlocked?, #id, #title>
end
runtime-analysisinferred · nothing written

A value's type is the messages the code sends it. Sorbet and RBS make you write that down and keep it in sync. Runtime Analysis reads it off real runs, following a return to where it's used next.

Static types make you name every interface

The strength of duck typing is the interface you never declare. Enrollment#unlock_next returns a list whose elements only ever get #unlocked? and #id. In practice three unrelated classes flow through it, Book, SampleBook and ArchivedBook, with no shared base class. That is polymorphism with nothing written down.

To type it, Sorbet or RBS has to turn that implicit interface into a nominal one: declare an Unlockable interface and make every class include it (and every future one), or hand-maintain a union T.any(Book, SampleBook, ArchivedBook). The open, structural interface becomes a closed, declared one that classes have to opt into.

Sorbet · to type it, name it
◆ unlockable.rb

1

2

3

4

5

6

7

8

module Unlockable
  extend T::Helpers
  interface!
  sig { abstract.returns(T::Boolean) }
  def unlocked?; end
  sig { abstract.returns(Integer) }
  def id; end
end  # then include it in every class, forever
Sorbet / RBSevery class must opt in
runtime-analysis · observed
runtime-analysis · real_implementers
$ ra real_implementers Enrollment#unlock_next

the call sends   #unlocked?  #id
answered by      Book · SampleBook · ArchivedBook
                 across 3,181 runs

# no interface declared, no class opted in

Runtime Analysis reads the implicit interface straight from usage: the call sends #unlocked? and #id, and the concrete classes that answered were Book, SampleBook and ArchivedBook. Nothing declared, nobody opted in. A new class that responds correctly just works, and shows up the next time it runs.