runtimeanalyz.ing

← Runtime Analysis

How Runtime Analysis compares with Sorbet and RBS.

Ruby's types are structural: a value's type is the set of messages it answers. With Sorbet or RBS you declare that type in a signature and update the signature when the code changes. Runtime Analysis reads the same contract from recorded runs, including the implicit interfaces your code relies 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

10

11

12

▶ reached by 4 examples · 12 app runs
def unlock_next
  curriculum.unlocked_for(student)
end
⬡ runtime_analysis hover

Enrollment#unlock_next · 2 callers
returns Array (16)
elements Book (32), Symbol (1) → #unlocked? #title

runtime_analysis · lspobserved · nothing written

Runtime Analysis treats a value's type as the messages the code sends it, and it follows a returned value to where it is used next.

Typing an implicit interface

Duck typing lets code depend on an interface that is declared nowhere. Enrollment#unlock_next returns a list, and ProgressController#update sends its elements two messages, #unlocked? and #title. No code requires the elements to be Books, so any class that answers both messages works. In the example app one class does, and the recording also shows the Symbol that an inaccurate stub returned.

To type this, someone writes the interface down. In Sorbet it becomes a nominal one: you declare an Unlockable interface and include it in every class that answers, including classes added later, or you maintain a union of those classes by hand. RBS can describe it structurally, as an interface type that needs no include, and the signature is still a file you write and keep in step with the code.

Sorbet · a declared interface
◆ 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(String) }
  def title; end
end  # then include it in every class
Sorbetevery class must opt in
runtime-analysis · observed
runtime_analysis · find_implementers
$ ra find_implementers '#unlocked?, #title'

real types responding to {#unlocked?, #title}
· 1 class
  Book  4  630  outside traced code (a factory or seeds)

# no interface declared, no class opted in

Runtime Analysis reads the implicit interface from usage. The call sends #unlocked? and #title, and Book was the one concrete class that answered both, in 630 instances from app runs. A new class that answers both needs no declaration and appears in this list the next time it runs.