Understanding RBS, Ruby's new Type Annotation System
Tags: ruby
Published: 2021-07-13 23:22:21

*This article was originally written by [Julio Sampaio][1] on the [Honeybadger Developer Blog][2].*

[RBS](https://github.com/ruby/rbs) is the name of a new type syntax format language for Ruby. RBS lets you add type annotations to your Ruby code in files with a new extension called _.rbs_. They look like this:

```ruby
class MyClass
  def my_method : (my_param: String) -> String
end
```

By providing type annotations with RBS you get benefits such as:

- a clean and concise way to define the structure of your codebase.
- a safer way to add types to your legacy code via files rather than changing the classes directly.
- the potential to universally integrate with static and dynamic type checkers.
- new features to deal with method overloading, duck typing, dynamic interfaces, and more.

But wait! Aren't there already static type checkers like [Sorbet](https://sorbet.org/) and [Steep](https://github.com/soutaro/steep)?

Yes, and they're great! However, [after four years](https://developer.squareup.com/blog/the-state-of-ruby-3-typing/) of discussion and a handful of community-built type checkers, the Ruby committer team thought it was time to define some standards for building type checker tools.

RBS is officially a language, and it's coming to life along with [Ruby 3](https://www.ruby-lang.org/en/news/2020/09/25/ruby-3-0-0-preview1-released/).

Plus, due to Ruby's dynamically typed nature, as well as common patterns like Duck Typing and method overloading, some precautions are being instituted to ensure the best approach possible, which we'll see in more detail shortly.

## Ruby 3 Install

To follow the examples shown here, we need to install [Ruby 3](https://www.ruby-lang.org/en/news/2020/12/25/ruby-3-0-0-released/).

You can do it either by following the [official instructions](https://www.ruby-lang.org/en/downloads/) or via [ruby-build](https://github.com/rbenv/ruby-build) in case you need to manage many Ruby versions.

Alternatively, you can also install the `rbs` gem directly into your current project:

```shell
gem install rbs
```

## Static vs Dynamic Typing

Before going any further, let's clarify this concept. How do dynamically typed languages compare to static ones?

In dynamically typed languages, such as Ruby and JavaScript, there are no predefined data types for the interpreter to understand how to proceed in case a forbidden operation happens during runtime.

That is the opposite of what's expected from static typing. Statically typed languages, such as Java and C, verify the types during compile time.

Take the following Java code snippet as a reference:

```java
int number = 0;
number = "Hi, number!";
```

This is not possible in static typing, since the second line will throw an error:

```text
error: incompatible types: String cannot be converted to int
```

Now, take the same example in Ruby:

```ruby
number = 0;
number = "Hi, number!";
puts number // Successfully prints "Hi, number!"
```

In Ruby, the type of a variable varies on the fly, which means that the interpreter knows how to dynamically swap from one to another.

That concept is commonly confused with _strongly typed_ vs _weakly typed_ languages.

Ruby is not only a dynamically but also strongly typed language, which means that it allows for a variable to change its type during runtime. It does not, however, allow you to perform crazy type mixing operations.

Take the next example (in Ruby) adapted from the previous one:

```ruby
number = 2;
sum = "2" + 2;
puts sum
```

This time we're trying a sum of two numbers that belong to different types (an `Integer` and a `String`). Ruby will throw the following error:

```text
main.rb:2:in `+': no implicit conversion of Integer into String (TypeError)
  from main.rb:2:in `<main>'
```

In other words, Ruby's saying that the job for performing complex calculations involving (possibly) different types is all yours.

JavaScript, on the other hand, which is weakly **and** dynamically typed, allows the same code with a different result:

```javascript
> number = 2
  sum = "2" + 2
  console.log(sum)
> 22 // it concatenates both values
```

## How does RBS differ from Sorbet?

First, from the approach that each one takes to annotating code. While Sorbet works by explicitly adding annotations throughout your code, RBS simply requires the creation of a new file with the _.rbs_ extension.

The primary advantage of this is when we think about the migration of legacy codebases. Since your original files won't get affected, it's much safer to adopt RBS files into your projects.

According to its creators, RBS’ main goal is to _describe the structure_ of your Ruby programs. It focuses on defining class/method signatures only.

RBS itself can't type check. Its goal is restricted to defining the structure as a basis for type checkers (like Sorbet and Steep) to do their job.

Let's see an example of a simple Ruby inheritance:

```ruby
class Badger
    def initialize(brand)
      @brand = brand
    end

    def brand?
      @brand
    end
end

class Honey < Badger
  def initialize(brand: "Honeybadger", sweet: true)
    super(brand)
    @sweet = sweet
  end

  def sweet?
    @sweet
  end
end
```

Great! Just two classes with a few attributes and inferred types.

Below, we can see a possible RBS representation:

```ruby
class Brand
  attr_reader brand : String

  def initialize : (brand: String) -> void
end

class Honey < Brand
  @sweet : bool

  def initialize : (brand: String, ?sweet: bool) -> void
  def sweet? : () -> bool
end
```

Pretty similar, aren't they? The major difference here is the types. The `initialize` method of the `Honey` class, for example, receives one `String` and one `boolean` parameter and returns nothing.

Meanwhile, the Sorbet team is working closely on the creation of tools to allow interoperability between RBI (Sorbet's default extension for type definition) and RBS.

The goal is to lay the groundwork for Sorbet and any type checker to understand how to make use of RBS' type definition files.

## Scaffolding Tool

If you're starting with the language and already have some projects going on, it could be hard to guess where and how to start typing things around.

With this in mind, the Ruby team provided us with a very helpful CLI tool called `rbs` to scaffold types for existing classes.

To list the available commands, simply type `rbs help` in the console and check the result:

![available commands](https://www.honeybadger.io/images/blog/posts/ruby-rbs-type-annotation/Figure1.png)
_Available commands in the CLI tool._

Perhaps the most important command in the list is `prototype` since it analyzes ASTs of the source code file provided as a param to generate "approximate" RBS code.

Approximate because it is not 100% effective. Since your legacy code is primarily untyped, most of its scaffolded content will come the same way. RBS can't guess some types if there are no explicit assignments, for example.

Let's take another example as reference, this time involving three different classes in a cascaded inheritance:

```ruby
class Animal
    def initialize(weight)
      @weight = weight
    end

    def breathe
      puts "Inhale/Exhale"
    end
end

class Mammal < Animal
    def initialize(weight, is_terrestrial)
      super(weight)
      @is_terrestrial = is_terrestrial
    end

    def nurse
      puts "I'm breastfeeding"
    end
end

class Cat < Mammal
    def initialize(weight, n_of_lives, is_terrestrial: true)
        super(weight, is_terrestrial)
        @n_of_lives = n_of_lives
    end

    def speak
        puts "Meow"
    end
end
```

Just simple classes with attributes and methods. Note that one of them is provided with a default boolean value, which will be important to demonstrate what RBS is capable of when guessing by itself.

Now, to scaffold these types, let's run the following command:

```shell
rbs prototype rb animal.rb mammal.rb cat.rb
```

You can pass as many Ruby files as you wish. The following is the result of this execution:

```ruby
class Animal
  def initialize: (untyped weight) -> untyped

  def breathe: () -> untyped
end

class Mammal < Animal
  def initialize: (untyped weight, untyped is_terrestrial) -> untyped

  def nurse: () -> untyped
end

class Cat < Mammal
  def initialize: (untyped weight, untyped n_of_lives, ?is_terrestrial: bool is_terrestrial) -> untyped

  def speak: () -> untyped
end
```

As we've predicted, RBS can't understand most of the types we were aiming for when we created the classes.

Most of your job will be to manually change the `untyped` references to the real ones. Some discussions aiming to find better ways to accomplish this are happening right now in the community .

## Metaprogramming

When it comes to metaprogramming, the `rbs` tool won't be of much help due to its dynamic nature.

Take the following class as an example:

```ruby
class Meta
    define_method :greeting, -> { puts 'Hi there!' }
end

Meta.new.greeting
```

The following will be the result of scaffolding this type:

```ruby
class Meta
end
```

## Duck Typing

Ruby doesn't worry too much about objects’ natures (their types) but does care about what they're capable of (what they do).

Duck typing is a famous programming style that operates according to the motto:

_"If an object behaves like a duck (speak, walk, fly, etc.), then it is a duck"_

In other words, Ruby will always treat it like a duck even though its original definition and types were not supposed to represent a duck.

However, duck typing can hide details of the code implementation that can easily become tricky and difficult to find/read.

RBS introduced the concept of _interface types_, which are a set of methods that do not depend on any concrete class or module.

Let's take the previous animal inheritance example and suppose that we're adding a new hierarchical level for terrestrial animals from which our `Cat` will inherit to:

```ruby
class Terrestrial < Animal
    def initialize(weight)
        super(weight)
    end

    def run
        puts "Running..."
    end
end
```

To avoid non-terrestrial children objects from running, we can create an interface that checks for the specific type for such an action:

```ruby
interface _CanRun
  # Requires `<<` operator which accepts `Terrestrial` object.
  def <<: (Terrestrial) -> void
end
```

When mapping the RBS code to the specific run method, that'd be the signature:

```ruby
def run: (_CanRun) -> void
```

Whenever someone tries to pass anything other than a Terrestrial object to the method, the type checker will make sure to log the error.

## Union Types

It's also common among Rubyists to have expressions that hold different types of values.

```ruby
def fly: () -> (Mammal | Bird | Insect)
```

RBS accommodates union types by simply joining them via _pipe_ operator.

## Method Overloading

Another common practice (among many programming languages actually) is to allow for method overloading, in which a class can have more than one method with the same name but the signature is different (like the types or number of params, their order, etc.).

Let's take an example in which an animal can return its closest evolutionary cousins:

```ruby
def evolutionary_cousins: () -> Enumerator[Animal, void] | { (Animal) -> void } -> void
```

In this way, RBS allows us to explicitly determine whether a given animal will have a single evolutionary cousin or a bunch of them.

## TypeProf

In parallel, the Ruby team has also started a new project called [typeprof](https://github.com/ruby/typeprof), an experimental type-level Ruby interpreter that aims to analyze and (try to) generate RBS content.

It works by abstract interpretation and is still taking the first steps towards better refinement, so be careful when using it for production purposes.

To install it, simply add the gem to your project:

```ruby
gem install typeprof
```

Be aware that it requires a Ruby version greater than 2.7.

Take the following version of the `Animal` class:

```ruby
class Animal
    def initialize(weight)
      @weight = weight
    end

    def die(age)
      if age > 50
        true
      elsif age <= 50
        false
      elsif age < 0
        nil
      end
    end
end

Animal.new(100).die(65)
```

Based on what's going on inside the method `age` and the further call to the same method, TypeProf can smartly infer the types manipulated in the code.

When you run the `typeprof animal.rb` command, that should be the output:

```ruby
## Classes
class Animal
  @weight: Integer

  def initialize: (Integer weight) -> Integer
  def die: (Integer age) -> bool?
end
```

It's a powerful tool that has a lot to offer for projects that already have lots of code going on.

## VS Code Integration

Currently, there are not a lot of available VS Code plugins for dealing with RBS for formatting, structure checking, etc. Especially because it's still relatively new.

However, if you search in the store for "RBS" you may find one plugin called _ruby-signature_ which will help with syntax highlighting, as shown below:

![RBS syntax highlighting](https://www.honeybadger.io/images/blog/posts/ruby-rbs-type-annotation/Figure2.png)
_RBS syntax highlighting in VS Code._

## Conclusion

RBS is just so fresh and already represents an important step towards safer Ruby codebases.

Normally, in time new tools and open-source projects will rise to back it up, such as the [RBS Rails](https://github.com/pocke/rbs_rails) for generating RBS files for Ruby on Rails applications.

The future holds amazing stuff for the Ruby community with safer and more bug-free applications. Can't wait to see it!

## About Honeybadger
[Honeybadger][3] has your back when it counts. We're the only error tracker that combines exception monitoring, uptime monitoring, and cron monitoring into a single, simple to use platform.  

[1]: https://www.honeybadger.io/blog/ruby-rbs-type-annotation/#authorDetails
[2]: https://www.honeybadger.io/blog/ruby-rbs-type-annotation/
[3]: https://www.honeybadger.io/