Past, Present, and Future of Sorbet Type Syntax
26 points by briankung
26 points by briankung
This is probably the best article I’ve read on ruby types so far. I still think typing for ruby is an ugly, useless mess - but this gives me some hope that things are finally improving to a point where all rubists might enjoy using types.
I’m still a bit envious of how python managed to solve this rather elegantly - especially merging typing and introspection as demonstrated by fastapi for example.
The verbosity and redundancy required to expose a typed rest or graphql api in ruby is just ridiculous.
A passing thought I had about Ruby types: I care much less about the data and more about the behavior of an object. What I really want is a rust trait style “trait only” type system. Rust traits are amazing and feel like strict duck typing, which is Ruby’s whole thing.
First, thank you for commenting on semantics—it’s clear you got the point I was trying to make in the article!
Traits and type classes are cool, but they’re a bit of an impedance mismatch for a language like Ruby that leans so heavily into runtime-only.
The feature itself is powered by an analysis pass that populates what would essentially be the dispatch table in an object-oriented language. Since the search is ahead-of-time, each object doesn’t carry around a vtable that says “there is exactly one implementation of this method” meaning that you can have code like this, where a single struct implements a method with the same name two different ways:
I first came to understand this from this article showing how languages with type classes use this analysis pass to essentially create the vtable, thread it through each method, and then use constant folding to avoid having to actually allocate a dictionary when the method is called.
I would be surprised to see a successful attempt to add typing to Ruby that used the trait/type class mechanism for static duck typing, because there is no ahead-of-time compilation step that would prepare these dictionaries and constant fold them away.
TypeScript (and Steep for that matter) approach the problem of “statically typed duck typing” differently: both allow ad hoc ways of specifying types for “any object, so long as it has this method with these types.” These type systems are said to implement structural interfaces, not the nominal interfaces that Sorbet implements.
TBH I think that Ruby’s “duck typing” via method existence is a little too open to be a truly good bounds. For example, almost everything implements to_s
and most things implement to_a
but the results and outcomes can be wildly different and varying.
You mention the same method with two implementations. I think that the intent of the trait is important. As in Display
and Debug
are both ways to turn something into a string, but one says “I am meant to be seen by an end-user” and the other says “I am meant to be seen by the developer.” And since that’s not an already-existing concept, it would need to be roughly invented from scratch to get the kind of safety that would be helpful/productive. Ruby has refinements which, if you squint enough, allow for multiple method definition, and while they definitely were not intended to be used as some kind of a “trait interface,” you could perhaps view them that way. I.e. if you call to_s
inside of a scope that is using ToCacheKeyString
then the intent is that the output string will be used in generating a cache key (for a strawman example). But there’s no way to call an inline refined method like you can with Rust’s <self as Display>::to_string()
.
I’m fascinated to learn that Stripe engineers want better docs. The external Stripe docs are often better than anything else I’ve seen.
There’s a lot to touch on: