First impressions of Sorbet, a Ruby type checker
2019-07-03Sorbet is a type checker for Ruby, built by Stripe. Its release has been long-anticipated, having been discussed at a handful of Ruby conferences, and being available to test out in an online playground called sorbet.run.
Now it’s been released as an open-source project, and I’ve been eagerly playing around with it. I’ll start off by saying that, overall, my experience with Sorbet has been quite positive.
I’ve always been a fan of static typing, and the lack of it in Ruby has sometimes put me off writing large projects with it. Sorbet could be enough to tempt me back - but only after it’s been given a bit of time to mature. In this post I’ll be discussing my thoughts on Sorbet in-depth.
What I like
It’s still Ruby!
This is one of the nicest things about Sorbet, made possible by Ruby’s flexible and versatile syntax. Ruby code which uses Sorbet is still just Ruby. Type signatures are added through method calls above each definition:
sig { params(a: Integer, b: Integer).returns(Integer) }
def add_ints(a, b)
a + b
end
All you need to run this is the sorbet-runtime
gem, which defines sig
and any other calls which are sometimes used to declare types.
This is in stark contrast to TypeScript’s approach, which is an entirely new language compiling down to JavaScript. Creating a new language adds an entirely new build step and complicates things somewhat. (In many ways, what TypeScript is to JavaScript is very similar to what Sorbet is to Ruby, so I’ll be drawing quite a few comparisons between them here.)
It’s gradual
Once you add Sorbet to your project, you don’t need to go through and add type signatures to every single method immediately. (However, shameless plug, my project will do that if you have YARD docs!)
Instead, you can gradually add types to your existing codebase, and Sorbet will only check methods with type signatures.
It brings an awesome language server with it
Sorbet needs to have a deep understanding of the structure of your codebase to do its job, and as a great bonus feature it is exposes this information over the Language Server Protocol. This means that your editor can see this information and offer rich autocomplete, jump-to-definition, and more.
With the Sorbet extension for Visual Studio Code, I got some of the best autocompletion I’ve ever seen in an editor for Ruby. Other solutions I’ve tried for Ruby in VS Code didn’t even come close; I’d say Sorbet is about on par, if not better, than RubyMine’s completion.
It makes code safer
Obviously, being a static type system, Sorbet brings all the advantages of static typing to your Ruby code. Accurate type signatures with the aid of a type checker can spot many common errors within the code you’re writing.
What I don’t like as much
It doesn’t support structural interfaces
I feel like this is currently one of Sorbet’s largest weak points. One of the aspects where TypeScript really knocked it out of the park is in supporting all of the stuff that people frequently do in JavaScript.
Like Ruby, JavaScript is quite a malleable language, but TypeScript is able to encompass virtually all of JavaScript’s behaviours. It does this by using structural typing with interfaces. The programmer can create an interface type defining properties and methods, and then all objects which contain these properties and methods match the interface type automatically.
For example, take this TypeScript code:
interface CanSpeak {
speak: (msg: String) => any;
}
class Person {
constructor(public name: String) {}
speak(msg: String) {
console.log(msg);
}
}
var speakable: CanSpeak = new Person("Aaron");
speakable.speak("Hello!");
Although we have not explicitly said that Person
implements the method required for the CanSpeak
interface, the TypeScript compiler can determine this for itself, and the type check succeeds.
On the other hand, Sorbet uses a nominative type system, more similar to what you may find in C# or Java. Interface implementation must be explicit, as you can see in this snippet of Ruby with Sorbet:
module CanSpeak
extend T::Helpers
extend T::Sig
interface!
sig { abstract.params(msg: String).returns(T.untyped) }
def speak(msg); end
end
class Person
extend T::Sig
include CanSpeak # <-- explicit interface inclusion
def initialize(name)
@name = name
end
sig { params(msg: String).void }
def speak(msg)
puts msg
end
end
speakable = T.let(Person.new("Aaron"), CanSpeak)
Without the include CanSpeak
line, this snippet does not pass type checking.
The issue with this system is that it means portions of code may have to be refactored entirely to include a particular interface. The even larger problem is that, if the code isn’t your own, you can’t edit that to include the interface! This could lead to tricky scenarios when adopting Sorbet in a larger codebase which depends on gems.
It doesn’t support options hashes very well
As you may well know, it’s extremely common practice for a method to accept an options hash: a hash parameter containing any extraneous options for that method. For instance, in an API library, you might see a method definition like:
def connect(endpoint, opts={})
# ...
end
You could specify values like your API key in the opts
hash. This helps to keep method signatures concise for methods with huge numbers of optional arguments.
Disappointingly, Sorbet doesn’t support hashes like this very well. You have a few options of what to use in your type signature, none of which are perfect:
Hash
, which just accepts any keys or values. This is almost like having no type checking at all.T::Hash[Symbol, T.untyped]
, which restricts keys to symbols but still allows any value and doesn’t enforce that certain keys are specified.- Use a shape, which allows both key and value types to be specified. However, you can’t have optional keys, which is one of the most common use cases of these options hashes.
What’s not ready yet, which is holding me back from using Sorbet everywhere
Most gems don’t have types yet
Sorbet has only just been released, and as such it hasn’t been adopted in most projects yet. This kind of adoption takes time, and I have no doubt that the Ruby community will come together to add type signatures to most popular gems in no time.
Plugins aren’t ready
Ruby’s wealth of metaprogramming-powered domain specific languages are one of the things that makes Ruby so special. Naturally, it’s not really possible to add type safety to these using a traditional type system, so Sorbet will have a plugin system to allow types to be generated on-the-fly.
This plugin system is currently not finalised and has some serious drawbacks. With that said, once the plugin system is fully implemented and matured, it will be really interesting to see how extensively static typing can be employed across existing Ruby tools.
In conclusion
Sorbet is a fantastic start in adding static typing to Ruby; the team at Stripe have done a brilliant job! It’s currently great for many small projects, but for larger ones I believe it needs time to fully mature and build up a community following. I’m looking forward to seeing how Sorbet develops in the near future.