We're pleased to announce Ruby 3’s new language for type signatures, RBS. One of the long-stated goals for Ruby 3 has been to add type checking tooling. After much discussion with Matz and the Ruby committer team, we decided to take the incremental step of adding a foundational type signature language called “RBS,” which will ship with Ruby 3 along with signatures for the stdlib. RBS command line tooling will also ship with Ruby 3, so you can generate signatures for your own Ruby code.
Typed versus untyped is a 30-year-old issue for programming languages. Typed languages are suitable for larger projects but are often less flexible. Untyped languages allow for rapid development, but scaling teams and codebases with them can be difficult.
Programming language designers are aware of these tradeoffs, and try to incorporate features of the other to offset this. C# has a feature called dynamic which delays type checking from compile time to runtime. Assigning and reading any type of value is allowed at compile time but may raise a runtime error to ensure safety. This is almost equivalent to untyped languages! How about the opposite? We see untyped languages type checking options (PHP, Python). We also have typed dialects of untyped languages that are used in production (TypeScript).
Matz declared that Ruby 3 will support static type checking four years ago. After seeing multiple community developed type checkers, the Ruby committer team decided to build a foundation for the community to build type checkers on. Ruby 3 will ship with the ability to write type signatures for Ruby programs as well as built-in type signatures for the Ruby standard libraries. The standard type signature language will make type definitions in Ruby code portable between type checkers and encourage the community to write types for their gems and apps.
We call the language and the library RBS.
We defined a new language called RBS for type signatures for Ruby 3. The signatures are written in .rbs
files which is different from Ruby code. You can consider the .rbs
files are similar to .d.ts
files in TypeScript or .h
files in C/C++/ObjC. The benefit of having different files is it doesn't require changing Ruby code to start type checking. You can opt-in type checking safely without changing any part of your workflow.
The type signatures for Ruby classes in RBS will look like this.
# sig/merchant.rbs class Merchant attr_reader token: String attr_reader name: String attr_reader employees: Array[Employee] def initialize(token: String, name: String) -> void def each_employee: () { (Employee) -> void } -> void | () -> Enumerator[Employee, void]
end
The merchant.rbs
file defines a class called Merchant
, and it helps the reader to understand the overview of the class.
The class has three attributes token
, name
, and employees
. The type of token
and name
are String
. RBS also supports generic classes like Array
as we can see with the type of employees
attribute. It is an Array
of Employee
s.
RBS also describes methods defined in the class and their types. The class defines the initialize
and each_employee
methods. The initialize
method requires token
and name
as keyword arguments. The each_employee
method accepts a block, or it returns an Enumerator
instance.
RBS is a language to describe the structure of a Ruby program. It gives developers an overview of the code and what classes and methods are defined. The biggest benefit is that the type definition can be validated against both the implementation and its execution!
The development of a type system for a dynamically typed language like Ruby differs from ordinal statically typed languages. There's a lot of Ruby code in the world already, and a type system for Ruby should support as many of them as possible.
This forces type system designers to make compromises on complexity and correctness for compatibility with existing code. We may have to introduce a type checker feature to support a pattern in existing Ruby code that may be incorrect otherwise. However, adding features makes the type system complicated and difficult to understand. So, we have focused on the most important code patterns to minimize the complexity of the type system.
We can show two of the important characteristics of Ruby code and how we can give types for them.
Duck typing is a popular programming style among Rubyists that assumes an object will respond to a certain set of methods. The benefit of duck typing is flexibility. It doesn't require inheritance, mixins, or implement declarations. If an object has a specific method, it works. The problem is that this assumption is hidden in the code, making the code difficult to read at a glance.
To accomodate duck typing we introduced interface types. An interface type represents a set of methods independent from concrete classes and modules.
If we want to define a method which requires a specific set of methods we can write it with interface types.
interface _Appendable # Requires `<<` operator which accepts `String` object. def <<: (String) -> void
end # Passing `Array[String]` or `IO` works.
# Passing `TrueClass` or `Integer` doesn't work.
def append: (_Appendable) -> String
This is better than traditional duck typing as it defines an explicit interface a class or module is expected to implement and provides hints for documentation and editor plugins to expose the formerly implicit interface as solid actionable documentation.
Non-uniformity is another code pattern of letting an expression have different types of values. It's also popular in Ruby and introduced:
To accommodate for this RBS allows union types and method overloading.
class Comment # A comment can be made by a User or a Bot def author: () -> (User | Bot) # Two overloads with/without blocks def each_reply: () -> Enumerator[Comment, void] | { (Comment) -> void } -> void ...
end
Union types and method overloading are commonly seen in Ruby code and standard libraries.
We provide a language to write types. So, what can we do with RBS files?
The following is a list of major benefits of having types. We can write types in RBS files, and the tools will help you writing Ruby code by:
nil
. Type checkers can check the possibility of an expression to be nil
and uncovers undefined method
(save!)' for nil:NilClass`.Of course none of this comes for free. How are we building tools for RBS to make work easier for developers to start using it?
We developed static type checkers on the top of RBS. Steep is the static type checker implemented in Ruby and it is based on RBS. Sorbet is a static type checker which has its own type definition language called RBI, but has plans to support RBS in the future.
We are also developing and working on additional tools to expand the RBS toolchain. RBS runtime type checker is one of the Ruby Google Summer of Code projects, which uses RBS type signatures to implement runtime type checking. type-profiler is an exploratory project to generate RBS files from Ruby source code based on a program analysis technique called Abstract Interpretation. There is also a project for Rails support.
This post introduces RBS, a new part of Ruby 3 for types. I explained what you can write using RBS, the key concepts of the design of RBS, and the benefits and tools that come with RBS. You write type definitions for your Ruby code, and our tools will analyze your code. We know not all of Rubyists will switch to typed Ruby, but we believe that it's worth trying with your code!
At Square, we are testing RBS based type checking solutions and continuing to iterate on them. We are writing RBS files for some internal projects and type checking the code with Steep. We are building RBS generators from .proto files.
I am looking forward to sharing the results of these experiments within a few months.