This article was originally written by @jnichiro in Japanese and translated into English by me (@suusan2go). You can find the original article here. Rubyで型チェック！動かして理解するRBS入門 〜サンプルコードでわかる！Ruby 3.0の主な新機能と変更点 Part 1〜
The original article was written before the official release of Ruby 3.0 so he used Ruby 3.0.0dev.
Ruby 3.0 introduces a mechanism called RBS that provides type definition information to Ruby code. This article explains the usage and roles of RBS and its surrounding tools through a simple sample program.
Ruby 3.0 has not yet been officially released at the time of writing this article. The actual behavior may differ from this article after the official release or future version upgrades. In this article, I used these versions.
Since these gems (RBS, TypeProf, and Steep) provide type-checking features, you can also try these features in Ruby 2.7 by installing these gems. I'll explain the details later.
You can find the sample code used in this article in this repository.
Let's create your own simple Ruby class and define the type to see how Ruby type checking works. You can try Ruby 3.0 type checking on your own by following steps.
First, create a directory for our experiment. In this case, create a directory called rbs-sandbox.
And then create a lib directory and create fizz_buzz.rb in it.
Next, Write the following code in lib/fizz_buzz.rb,
We can write your type definitions for the
FizzBuzz.run method manually, but this time let's use the TypeProf command that comes with Ruby 3.0 to generate the type definitions automatically.
To automatically generate type definitions with TypeProf, we need Ruby code to execute the target code.
So let's write the following code in runner/fizz_buzz_runner.rb. (Note: The directory name "runner" is a random name, not a reserved word for RBS or anything. )
To be sure, let's also check that the above code works. If you get a result like the following, you are good to go.
Now we are ready to run TypeProf. Enter the following command, and you will get the following output. (Note: At the time of writing this article, a large number of strange warnings are also output: unknown RBS type: RBS::Types::Bases::Top)
The output result is the type definition of the FizzBuzz class, which is automatically type-inferred by TypeProf.
As shown in
(Integer) -> Array[String], it expresses that passing an argument of type Integer will return an Array of String as the return value.
Next, let's use a gem called Steep to try static type checking on our program. Ruby 3.0 doesn't include Steep, so we need to install it manually.
After installing the gem, run the steep init command.
This command will create a Steepfile like the below.
The Steepfile is a configuration file that Steep requires. At first, the example configurations are commented out. Let's rewrite this Steepfile as follows.
The above configuration means that the lib and runner directories are for type checking, and the sig directory is for type definitions. (Note: At the time of writing this article, I could not find any explanation about DSL in Steepfile, so I guessed the meaning of the setting)
Let's add the type definition file in the sig directory. Make a sig directory and fizz_buzz.rbs file in it. Then, write the type definitions from TypeProf into fizz_buzz.rbs.
You are now ready to use Steep. Now, enter the command "steep check".
Did anything happen? I believe nothing is happening. If nothing happens, it means that the type check was successful. To see if Steep can detect an invalid type, change fizz_buzz_runner.rb as follows.
You should get an error when checking the type because you passed String instead of Integer as the argument.
After checking this error, please undo the code in fizz_buzz_runner.rb.
Steep also checks the type of Ruby's built-in libraries. To confirm this, let's call the "upto" method on a string in fizz_buzz.rb as follows.
You should get an error from the type checking. (Note: At the time of writing this, the same error message is printed twice for some reason.)
After checking this error, please undo the code in fizz_buzz_runner.rb.
In this article, I'll call a class that we can use without require, such as String and Integer, "built-in libraries" (or core libraries), and a class that requires "require", such as Date and Pathname classes, "standard libraries".
In this section, I'll explain the procedure to run type-checking for standard libraries.
At first, let's change the "fizz_buzz_runner.rb" to the following. In this case, we won't use the fixed number like 15 but will use the current day (1 to 31) to repeat the FizzBuzz function.
Then, let's run Steep. There is nothing wrong with the code, so nothing happens. Now let's change .day to .dy.
#dy doesn't exist in Date class, so you should get an error.
oops? Nothing happened again. Actually, to check the type of standard library, it's necessary to specify its name in the Steepfile. So let's add the following line to the Steepfile.
Now steep can detect type errors like this.
When Ruby 3.0 is released, Ruby will include type definition information from the beginning for built-in libraries and standard libraries. However for the other libraries like third party gems, we need additional type definitions.
When writing this article, only a few gem types, such as listen, rainbow, Redis, and retryable in the gem_rbs_collection. But as RBS becomes more popular, the number of type information will increase. (Of course, you can contribute!)
Let's try to use the Retriable gem's type definition. First, install the Retriable gem.
Next, rewrite fizz_buzz_runner.rb as follows. (This code itself doesn't make much sense because we just want to check the type of Retriable. )
Then, use the git submodule to import gem_rbs_collection.
$ git init $ git submodule add https://github.com/ruby/gem_rbs.git vendor/rbs/gem_rbs
Edit Steepfile as follows to load type definition for Retryable.
We have to add
library "forwardable" because Retryable is using the Forwardable module internally. If we don't add this line, Steep will return errors like
UnknownTypeNameError: name=Forwardable when running
If we run
steep check this time, nothing should happen because there are no type errors.
Then, Let's rewrite .retryable to .retryablee.
You can see an error like the below because Retryable doesn't have a method like
Steep will check keyword arguments as well. Let's change 3 to the string "3".
steep check will return an error and tell us the type of keyword argument is wrong.
You can find this extension by searching "Steep". Let's install this.
You need to set up bundler to use this extension. Let's setup Gemfile by running
Then edit Gemfile and run
Open our rbs-sandbox directory by VSCode, and now you can see type errors on VSCode!
The autocomplete function for method names also works (VSCode displays type information at the same time).
If you hover the cursor over a method, VSCode will also display the type information for that method.
As already explained, TypeProf can be used to generate type information automatically. However, since TypeProf guesses the type from the code, sometimes the type information may not same as intended by the programmer.
For example, let's check the following code.
In this case, TypeProf will generate the type information as follows.
The initialize method return String type, but most of Ruby programmers intended to "just set the name to the instance variable" not to return the String value.
In this case, you need to edit the output result of TypeProf yourself. You can change the return value of sig/person.rbs from String to void and saved it as follows.
Next, implement the hello method in the Person class and call it in person_runner.rb
If you rerun TypeProf in this state, the return type of initialize method will be overrided to String again. It is troublesome to modify the previously modified content over and over again every time you run TypeProf.
To avoid this problem, TypeProf allows you to use existing rbs files as input for type inference.
Let's run TypeProf with the person.rbs file we just created as the argument. In this way, only the type information that we don't define in the existing RBS file will be output (TypeProf will comment out current type information).
Also, TypeProf can output the automatically generated type information to a file by adding the -o option.
So far, we have used the TypeProf command to create a type definition file, but instead of TypeProf, we can use the rbs command to generate a type definition file prototype.
When using the rbs command, RBS doesn't infer type like TypeProf (both arguments and return value are output as untyped). Instead, you do not need to prepare a program (runner script in this article) to execute the target file.
The rbs command has several other functions besides creating a skeleton. The following is an example of using the rbs command to check method signatures.
In this article, I created a script such as fizz_buzz_runner.rb, but you can also use a test code such as Minitest to generate RBS.
It seems that you can use the rbs_rails gem developed by @pocke (@p_ck_) to generate rbs for Rails, but I haven't tested it yet. I'm sorry. For more information, please refer to the rbs_rails repository and various web articles.
If you've been interested in Ruby type checking for a while, you may have heard of Sorbet.
Sorbet is a type-checking tool that has been developed before RBS and Steep. Sorbet and RBS/Steep have the same purpose of "type checking for Ruby", but the mechanism to achieve this is different. The sorbet team and Ruby team will be working together to build a mutually compatible type checking mechanism. For more information, please see the following blog post (these are in English).
Not only steep, but RBS and TypeProf are provided as gem. Therefore, you can use these tools not only in Ruby 3 but also in Ruby.27. The followings are the result of these tools in Ruby 2.7.
I'm not sure precisely what RBS stands for, but I'm guessing it stands for "Ruby Signature language". from this slide.
In this article, I just introduced a "Hello World" for these tools. If you want to use the type-checking feature in production code, you might need more deep knowledge. If you want to know more, please refer to the following documents.
In each repository's README, You can find links to the more detailed documents.
You can try TypeProf in your browser at the following site. https://mame.github.io/typeprof-playground/
This article introduced Ruby's static type checking feature using RBS and related tools that Ruby3.0 provides. I tried to use RBS in some use cases, but if I wrote a bit complicated code, it makes it harder to write appropriate type definitions, and sometimes I got unexpected type errors. Honestly, it's a little bit hard to introduce these tools to a real project yet. Ruby is a highly dynamic language, so I felt providing a type definition from outside will be quite tricky. But I think it's a big step for Ruby to have official support for type checking finally. As the number of gem supports type definitions gradually increases and knowledge of writing type definitions accumulates, static type checking in Ruby will be standard in a few years.
You can read my other article for Ruby3.0 here!