Statically typed code, especially when introduced incrementally to a codebase, can improve reliability and developer productivity while maintaining readability. Stripe calls Sorbet “doubling down on what makes Ruby delightful”. In this article we’ll explore what static typing is, how to add Sorbet to a Ruby on Rails application, and even how to run Sorbet in a continuous integration pipeline.
Dynamic Typing vs Static Typing
Programming languages can generally fall into one of 2 categories — Statically-Typed and Dynamically-Typed.
Ruby is a dynamically-typed language. This means that the Ruby interpreter does its best to infer the type of objects, on the fly, at runtime.
Statically-typed languages, such as Java, require that the programmer declares the type of an object when they declare the object.
Most Ruby programmers love dynamic typing. It leads to shorter, cleaner code that looks more like written English. It enables programmers to decouple types in method definition, leading to less complex diffs when changing the structure of a program. Still, dynamic typing presents some challenges.
The flexibility that comes with dynamic typing can sometimes come around to bite the programmer, or more likely, the user. Not having compile-time type-checking requires a programmer to know what type the objects they are working with have, or more importantly, what methods are available on that object. Type errors might only present in a subset of cases, making them particularly dangerous and tricky to track down.
How adopting static type checking affects site reliability
A lot of ruby programmers might kill me for saying this, but explicit types are good. Linus Torvalds was right when he called us “strange people”.
Static types reduce errors by catching misused types before runtime, which directly impacts users in a positive way. Trading some of what makes Ruby programmer-friendly in order to be user-friendly is a fair trade.
Stripe actually claims that the primary goal of Sorbet is to make programmers happier and more productive, and that increased reliability is a secondary benefit. With static types, you can define NameError: uninitialized constant errors almost completely out of existence.
Type checking doesn’t eliminate all errors. In fact it might actually introduce more errors in development as you add type annotations and discover objects being misused. It’s always better, however, that a programmer discovers these sorts of errors, rather than a user.
When production incidents do happen, type annotations may make the errors even more helpful. Having some output that describes why code went wrong is undeniably better than NameError: uninitialized constant
.
How to add Sorbet to a Rails app
Adding Sorbet to an existing Ruby on Rails application can be done incrementally.
Sorbet actually comes as three gems, which you’ll need to add to your Gemfile
:
gem 'sorbet', :group => :development
gem 'sorbet-runtime'
gem 'tapioca', require: false, :group => :development
The Sorbet gem runs as an executable from the command line. The sorbet-runtime gem provides for runtime
syntax. And tapioca
is a tool for generating .rbi
files. After adding these to your Gemfile, install them by running:
bundle install
After installation, initialize sorbet with tapioca by running:
bundle exec tapioca init
At this point, you may see a fair deal of errors if you’re in a rails project. The rails framework uses quite a fair deal of metaprogramming, meaning a lot of methods don’t technically exist until runtime. If methods don’t exist, they’re kind of hard to type check.
Non-rails Ruby projects won’t quite have as much of a problem here, but it is a challenge when adding sorbet to a rails project. Fortunately, the tapioca gem makes some of this easier. With a series of commands, we can generate the necessary .rbi
files for sorbet to run type checking for the first time. To do this, run the following in your project’s directory:
bin/tapioca gems
Then run:
bin/tapioca requir
Then run:
bin/tapioca dsl
This should be enough for you to run srb tc
without errors for the first time.
If you are still getting some errors, but you want to move on, you can type check a single file by going to the directory running srb tc <path-to-file>
from outside the project. Note that this will still prove troublesome for Rails, applications as Sorbet will not be aware of types defined outside of the given file.
By default, Sorbet actually suppresses type checking warnings. To enable them, you’ll need to add a comment to the top of a given Ruby file.
Adding # typed: true
to the top of a file will cause Sorbet to report errors on things like non-existent methods and incorrect argument counts.
Adding # typed: strict
will require that all methods have method signatures and that all instance variables have explicit types.
You can read more about enabling static type checking in Stripe’s docs here as well as insights into writing method signatures.
Running Sorbet in CI
One great benefit of running type checking from the command line is it allows easy integration into your existing continuous integration system. This can ensure that existing standards are enforced before code is merged into the main branch.
If you don’t already have an existing continuous integration pipeline, you can get started quickly with SemaphoreCI.
First, sign up for semaphore – it’s best to sign up through Github to connect to your project easier. Click “Create New”, and select “Choose Repository” to connect Semaphore to an existing Github repository.
Select the basic “Ruby on Rails” workflow and edit it to add a line for bundle exec srb tc
:
checkout
sem-version ruby 3.1.0
cache restore
bundle install --deployment --path vendor/bundle
cache store
bundle exec srb tc
Conclusion
Microsoft already evolved JavaScript with TypeScript, which is increasing in popularity. Dynamic typing is one of the things that makes Ruby such a joy to program with, so to some it may seem counterintuitive to introduce types. Still, introducing types into Ruby code leads to safer code and arguably more productive programmers.
Sorbet allows us to gradually introduce static typing into an existing Ruby codebase. Sorbet suppresses type warnings by default, so adding it to a new Ruby codebase is not overwhelming. Adding type checking file-by-file with increasing degrees of strictness presents a good opportunity for incremental adoption.
Using Sorbet in a Rails codebase presents a set of challenges that we can work through, usingTapioca to empower us to begin type checking in a way that will lead to decreased (but more helpful) errors and (ideally) a better developer experience. Rails relies on a great deal of metaprogramming, which is challenging to type-check. When adding Sorbet to a Ruby on Rails project, you immediately face a number of errors that would take quite a bit of time to resolve manually. Fortunately, Tapioca makes this easier by generating interface files that allow Sorbet to run without errors for the first time.