Unleashing the Power of Type Checking in Ruby with Sorbet

A powerful ruby static type checker to supercharge your IDE.
Asad Ahmed
Asad Ahmed
July 21, 2023

What is Sorbet

Sorbet is a fast, powerful type checker designed for the Ruby programming language. Developed by Stripe, it is an open-source tool that aims to make Ruby code safer and more understandable without sacrificing its flexibility and expressiveness. Sorbet achieves this by introducing gradual static typing to Ruby, allowing developers to annotate the types of method parameters, return values, and class attributes.

How Sorbet Works?

Sorbet works in two main phases: a static phase and a runtime phase.

Static Phase:
In this phase, Sorbet reads and analyzes the Ruby code, checking the type annotations written by developers. It performs this check before the code is run, hence "static." Developers use special comments called "sigs" (short for "signatures") to annotate the methods in their code. These signatures specify the types of parameters that a method expects and the type of value it returns. Developers have to do a little bit more work for extra clarity of the code. And the code might not look as good as pure Ruby.


# typed: true
extend T::Sig
sig { params(x: Integer, y: Integer).returns(Integer) }
def add(x, y)
x+y
end

In the above example, Sorbet will check that the ‘add' method is always called with two integers as argument and it returns an integer.

For the static typing, you can type check an entire file, just a method, for a single argument etc. At the file level, there are five strictness levels you can choose from ranging from ignoring all the type checking( basically turning it off) to strong. Here is the part of the documentation that goes deep into these levels.

Runtime Phase:
In addition to its static checking, Sorbet includes an optional runtime component. When enabled, this component checks that the types of actual values at runtime match the annotations.

This can catch discrepancies that the static phase might miss, especially in parts of the codebase that haven't been fully annotated.


require 'sorbet-runtime'
class Example
extend T::Sig
sig {params(x: Integer).returns(String)}
def self.main(x)
  "Passed: #{x.to_s}"
end
end
Example.main([]) # passing an Array!

So when you run this code, in addition to static type checked, it will also go through the runtime checking and will throw the following error due to the signature violation:


Parameter 'x': Expected type Integer, got type Array with unprintable value (TypeError)
Caller: example.rb:11
Definition: example.rb:6

Pros (Use Cases)

Preventing Runtime Errors:
Sorbet helps identify issues in the codebase that would result in ‘NoMethodErrors' or NameErrors in production, which is one of the most common and annoying errors in Production. By statically checking the types before runtime, Sorbet helps to prevent such errors, making the production environment more stable.

Improving Code Readability and Predictability:
Sorbet's explicit type annotations are designed to make the codebase more readable and easier for engineers to understand, especially in a team setting where most engineers spend time reading others' code. This aids in understanding not just what the code does, but what it was intended to do.

Facilitating Safe Refactoring:
Sorbet's type checks provide developers with the confidence to refactor code more aggressively, knowing that many issues will be caught before the code even runs.

Scaling with Codebase and Team Size:
As software projects grow, both in terms of code size and team members, new challenges emerge. Code that was once manageable and clear can become increasingly difficult to navigate, and the likelihood of bugs being introduced during development can rise significantly. Sorbet was Stripe's answer to these challenges.

Incremental Adaptation:
Sorbet allows for gradual introduction of static typing into an existing Ruby codebase, which can be a significant advantage for teams considering adopting a type checker. Unlike some tools that require a full, immediate commitment, Sorbet is designed to be non-disruptive when introduced into a project.

Sorbet is fast:
Sorbet is really fast in local development because it was written in C++ which offers direct compilation to native code and takes advantage of high-performance data structures. Furthermore, its design prioritizes cache locality optimizing CPU cache usage, which contributes to its increased speed and efficiency.

Cons

Complexity with Metaprogramming:
Ruby is known for its extensive use of metaprogramming, which allows for dynamic generation of methods and classes. Sorbet, as a static type checker, is designed to operate on code that can be analyzed before it runs. This can create friction when Sorbet encounters Ruby's dynamic features, as it has to determine the types of methods and classes that are generated at runtime.

Learning Curve:
Adopting Sorbet introduces a notable learning curve, especially for developers who are new to static typing. Developers will need to familiarize themselves with a new syntax for adding type signatures to their methods.

In addition to that, Sorbet offers various levels of strictness—from ignoring type errors completely to treating them as hard errors—that developers need to understand and apply effectively.

Initial Setup and Error Resolution:
Contrary to the optimistic scenario depicted in the documentation, the setup process can be more challenging. Tools like Tapioca assist in generating interface files for a Rails environment, but developers often still face a significant number of errors that require manual resolution. These errors can be overwhelming and may involve complex refactoring.

Value Delivered

For detailed values that Sorbet provides to the team and codebase, please refer to the pros section of this document. However, it is worth emphasizing two main values that Sorbet brings to the table. The most immediate value delivered by Sorbet is the increased safety it brings to a Ruby codebase. By checking types statically (and at runtime), Sorbet helps to catch many potential issues that could lead to bugs or crashes in production.

The second value I would like to mention is that Sorbet encourages following the best practices. By enforcing type checks, Sorbet encourages developers to think more critically about the data their code operates on. This often leads to more thoughtful, clearer, and more maintainable code.

Feasibility and Effort Required

Feasibility:
Sorbet is designed to be compatible with existing Ruby codebases. This makes it feasible for most Ruby projects to adopt Sorbet without requiring significant changes to the existing code. Additionally, Sorbet allows for gradual adoption. Teams can start with a small subset of files at a low strictness level, making it feasible to introduce Sorbet without a massive initial commitment. This also means the team can realize Sorbet's benefits before fully adapting it across the entire codebase.

Effort Required:
Although tools like Tapioca can generate initial type signatures for the codebase, developers need to go through the generated signatures, understand them and correct and refine them. If the gradual type checking is adapted and the codebase is targeted area by area, it might not require a prohibitive amount of effort to adapt Sorbet. However, it should be noted that it may take a long time until the codebase is fully type checked.

What is worth mentioning, though, is once adapted, dropping Sorbet and going back pure Ruby and Rails will prove an expensive undertaking since it affects the entire codebase.

Sorbet and Apple M1 chips

If you are using a machine with Apple's M1 chip, it's important to note that Sorbet has compatibility issues with this hardware. As of now, direct support for M1 Macs might not be readily available out of the box. Before integrating Sorbet into your codebase, ensure you verify the latest compatibility updates and solutions provided by the Sorbet community or its developers.

Closing Thoughts

Sorbet presents a compelling approach to bringing robust, static type checking into the dynamic world of Ruby. It offers a unique blend of rigor and flexibility that addresses many of the challenges large Ruby codebases can encounter, especially as they scale.

The key benefits of Sorbet – reducing runtime errors, aiding in code readability, enabling confident refactoring, gradual type-checking– make it an appealing tool for both small startups and large, established teams.

However, Sorbet is not a silver bullet. Its approach to metaprogramming—a staple in Ruby development—can introduce challenges. The initial setup may demand a significant effort, especially for large codebases.

In summary, Sorbet is a promising solution for Ruby teams aiming for long-term code quality and maintainability. However, it's important to note that M1 chip Macs are not natively supported out of the box at the moment. The decision to adopt Sorbet should balance the needs of the project with the team's readiness for a new approach to Ruby development, especially if team members are using M1-equipped machines. For those willing to invest in this process, Sorbet may prove to be a transformative tool, echoing the transformative impact TypeScript has had on the JavaScript world.

Interested in joining a mission-driven team committed to improving Canada? If so, please take a moment to look at our open positions!

About the author

Asad Ahmed

Asad is a Full Stack Developer based out of Kitchener, ON.