12 Jan 2023 · Software Engineering

    How to Use Sorbet to Type-Check Ruby

    6 min read
    Contents

    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.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Jeff is a Software Engineer writing code, fixing bugs, and helping patients get the medications they need to live healthy lives.
    Avatar
    Reviewed by:
    I picked up most of my soft/hardware troubleshooting skills in the US Army. A decade of Java development drove me to operations, scaling infrastructure to cope with the thundering herd. Engineering coach and CTO of Teleclinic.