Ruby 3 has introduced a new syntax language for dynamic typing called RBS. In short, it is a language that we can use to describe the data types used in a Ruby class. We can define the data type of variables and the return type of methods used in a class using RBS.

Why do we need type-checking?

Since Ruby is a dynamically typed language, we don't need to define the data type of the variables we are using. Ruby automatically assigns a type based on the variable's value at runtime. Let's take the below class as an example.

# basic_math.rb

class BasicMath
  def initialize(num1, num2)
    @num1 = num1
    @num2 = num2
  end

  def first_less_than_second?
    @num1 < @num2
  end

  def add
    @num1 + @num2
  end
end

If we run the below code it will return false as expected since integer 1000 is not less than integer 2.

puts BasicMath.new(1000, 2).first_less_than_second?
 
=> false

We did not define that num1 and num2 are integers. Ruby interpreter automatically assigned integer type to them at runtime. And the method first_less_than_second? checks if its first argument is less than the second one. But let's call the same method with strings as parameters.

puts BasicMath.new('1000', '2').first_less_than_second?
 
=> true

Here the output is true because num1 and num2 are strings. But for this method we expected the output to be false. This error happened because we missed doing a string-to-integer conversion on the parameters. A type-checking system will help us identify such errors.

How to write an RBS file?

Let's write an RBS file for the above BasicMath class.

# basic_math.rbs

class BasicMath
  @num1: Integer
  @num2: Integer

  def initialize: (Integer num1, Integer num2) -> void
  def first_less_than_second?: -> bool
  def add: -> Integer
end
  • For the variables @num1 and @num2, we define its type using the : symbol.
  • For methods, we write the method name followed by a : symbol and then specify the types of each method argument inside the () bracket. Then we define the return type using a -> symbol.

How to run an RBS file?

We only defined the structure of the BasicMath class in our RBS file. We still need to run it against a type checker to verify if the types are correct in our class. RBS is only a language that defines the structure of a class. It cannot do type-checking on its own. Let's use a popular type checker called steep to test our class against the RBS file.

Setup steep gem

  • Install steep gem using gem install steep.
  • Run steep init to generate the configuration file for steep.
  • This will generate a configuration file called Steepfile.
  • Add the following code to the Steepfile.
target :app do
  check "lib"
  signature "sig"

  library "set", "pathname"
end
  • Add the directory of the .rb files in check option and directory of .rbs files in signature option.
  • In this example, I am adding .rb files in lib directory and .rbs files in sig directory.
  • Make sure the file names of the respective .rb and .rbs files are the same.

Type-checking using Steep

Run steep using the command steep check. This will perform type-checking on all the Ruby files in lib directory, using their respective RBS files in sig directory.
If the type-checking passes it will return the below message.

steep check

# Type checking files:

....................................................................................

No type error detected. đź«–

If there is any mismatch in the types, it will throw an error message. For example, in BasicMath class, let's change the return value of first_less_than_second? to a String. In our RBS file, we have defined that the output of this method will be a Boolean.

# lib/basic_math.rb

class BasicMath
  def initialize(num1, num2)
    @num1 = num1
    @num2 = num2
  end

  def first_less_than_second?
    @num1 < @num2
    'yes'
  end

  def add
    @num1 + @num2
  end
end

Now if we run steep check, it will return an error message. This is because according to the RBS definition first_less_than_second? method is expected to return a Boolean value.

steep check

# Type checking files:

.......................................................................F............

lib/rbs_test.rb:7:6: [error] Cannot allow method body have type `::String` because declared as type `bool`
│   ::String <: bool
│     ::String <: (true | false)
│       ::String <: true
│
│ Diagnostic ID: Ruby::MethodBodyTypeMismatch
│
â””   def first_less_than_second?
        ~~~~~~~~~~~~~~~~~~~~~~~

Detected 1 problem from 1 file

TypeProf

TypeProf is a type analysis tool that comes built-in with Ruby 3. It can automatically detect data types in a class and return a rough RBS file for it.
Just run the command typeprof <filename> and it will return a rough RBS file. Let's try running the typeprof command on the above BasicMath class.

typeprof lib/basic_math.rb

This will return the following result.

# TypeProf 0.21.3

# Classes
class BasicMath
  @num1: untyped
  @num2: untyped

  def initialize: (untyped num1, untyped num2) -> void
  def first_less_than_second?: -> untyped
  def add: -> untyped
end

If we check the returned RBS code, we can see that TypeProf assigned untyped as the data type in some places. This is because TypeProf couldn't determine the correct data type at those places. But it did return a skeletal structure for the RBS file which we can edit according to the way we want. TypeProf is still an experimental tool, so it doesn't always return an accurate RBS file.

Conclusion

We saw how we can write an RBS file for a class and do type-checking using Steep. We also saw how we can use TypeProf to generate a rough RBS file for a class. Since RBS allows us to add type definitions, it can help in catching type errors in our code. So it is a good practice to write RBS for our Ruby code.

References