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 incheck
option and directory of.rbs
files insignature
option. - In this example, I am adding
.rb
files inlib
directory and.rbs
files insig
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.