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
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
1
2
3
class Enrollment def unlock_next = curriculum.unlocked_for(student) ⤷ Array<#unlocked?, #id, #title> end
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.
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
$ 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.