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
1
2
3
4
class Enrollment extend T::Sig sig { returns(T::Array[Book]) } def unlock_next = …
1
2
3
class Enrollment unlock_next: () -> Array[Book] end
10
11
12
▶ reached by 4 examples · 12 app runs def unlock_next curriculum.unlocked_for(student) end
Enrollment#unlock_next · 2 callers
returns Array (16)
elements Book (32), Symbol (1) → #unlocked? #title
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.
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
$ 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.