Type Checking

Static typing with RBS in Ruby

Static typing with RBS in Ruby

by Gaurav Kumar Singh

In this talk, Gaurav Kumar Singh explores static typing in Ruby using RBS at RubyConf 2022. He starts by defining the concepts of static and dynamic typing, explaining the issues associated with dynamic typing through examples in Java and Ruby. The main challenges of dynamic typing include runtime errors and the limitations of thorough testing. To address these, Singh introduces static typing as a way to catch errors early and improve code quality.

Key Points Discussed:
- Static Typing vs. Dynamic Typing: Static typing defines variable types at compile-time (e.g., Java) while dynamic typing determines types at runtime (e.g., Ruby).
- Advantages of Static Typing: It helps in eliminating runtime errors, enhances IDE integration, and improves documentation through type annotations.
- Overview of Ruby's Type Checkers: Singh discusses the evolution of type checkers in Ruby, including Diamondback Ruby, RDL, Sorbet, and RBS, which was introduced by the Ruby core team in 2019.
- Understanding RBS: RBS (Ruby Syntax) functions as a type definition file system rather than checking code types directly. It helps define the structure of Ruby code and can be generated using various methods like RB, RBI, and runtime.
- Creating RBS Files: Through a 'User' class example, Singh demonstrates how to generate RBS files and define types for class attributes to catch early errors.
- Dynamic vs. Static Attributes: He explains the nuances of generating RBS for dynamically defined attributes and methods, suggesting that runtime options might be more effective for such cases.
- Function Signatures in RBS: Various function examples illustrate how to use optional arguments, splat operators, union types, and keyword arguments.
- Introduction to Steep: Steep is mentioned as a tool for type-checking RBS code against Ruby, highlighting its capabilities for providing code coverage statistics and error management.
- Interoperability of Sorbet and RBS: Singh concludes by discussing the differences between Sorbet and RBS, emphasizing the annotation-based nature of Sorbet compared to RBS's definition file approach. Despite their differences, both systems can improve type-checking in Ruby.

The talk concludes with the affirmation that the adoption of tools like Sorbet and RBS can significantly enhance Ruby's type-checking capabilities, improve code robustness, and facilitate code management.

00:00:00 Ready for takeoff.
00:00:17 Good afternoon everyone, my name is Gaurav Singh and I'm an engineer at Pattern. Today, we are going to talk about static typing in Ruby using RBS.
00:00:23 So, first of all, what is this typing business? What do we mean by types? Broadly, there are two categories of programming languages: those which are strictly typed and those which are dynamically typed.
00:00:30 Let's take the first example of Java code. Here, a string variable named 'str' has the type string, which contains the value 'hello'. However, if we try to assign it a value of integer type, we get a syntax error. In dynamically typed languages, the type is associated with the value that the variable is holding.
00:00:50 For example, in Ruby code, the type of the variable 'name' is a string when it holds the value 'Who am I?', whereas its type is integer when it holds the value '5'. Therefore, in dynamically typed languages, type is associated with the value.
00:01:20 Now, what is the problem with dynamic typing? Let's consider a simple add function in Java, which accepts two arguments, both of which are integer types and returns their sum. If I try to pass those arguments with a string type, I'm going to get a syntax error.
00:01:31 Now, consider the second example with Ruby code. I am passing two arguments and summing them, but if I pass a string argument, it won't throw an error during coding but will give me an error at runtime. These are the kinds of issues we face with dynamic typing.
00:01:57 Some people argue that we can write extensive test code, but we all know that humans can make mistakes. To overcome these problems, static typing has been introduced.
00:02:21 So, what are the advantages of type checking? First, it's proven to be beneficial in multiple languages like JavaScript and Python, where there are many popular static type checking libraries. TypeScript is a well-known version for JavaScript, and MyPy is popular for Python.
00:02:42 Static type checking helps eliminate many errors, such as 'no method error' or 'nil class errors' at runtime, and it provides better integration with IDEs. For instance, integration with IDEs allows us to generate documentation for our code; by annotating our code with static typing, we automatically get implicit documentation.
00:03:06 Now, what type checking systems are available in Ruby? This isn't something new in the Ruby ecosystem. Back in 2009, there was something called Diamondback Ruby. Later, Tufts University came up with RDL, and then Stripe launched its type checker, Sorbet. In 2019, the Ruby core team introduced RBS.
00:03:27 So, what exactly is RBS? RBS stands for Ruby Syntax. It allows you to define your Ruby code structure in a type definition file, similar to JavaScript's .d.ts files, which we commonly use.
00:03:51 RBS does not check the type of Ruby code; rather, it provides a programming interface to define the structure of your Ruby code. It includes options for parsing code and creating an Abstract Syntax Tree (AST), which will be useful for annotating Ruby classes.
00:04:14 In this talk, we will focus on generating code for our Ruby code using RBS. We will look at various options available such as RB, RBI, and runtime, where 'RB' uses syntactical parsing of the Ruby code.
00:04:40 RBI is part of Sorbet, a popular library. RBI files are generated by Sorbet, and to convert RBI files into RBS, we generate those using specific commands. The runtime option loads your class into memory and then inspects all functions or methods and their attributes, generating the necessary RBS code.
00:05:18 Let's take a simple example. We have a class called 'User' with two attributes: name and age. We are going to generate the RBS file using the RB option. The output will look something like this: both attributes will initially be untyped.
00:05:45 However, the goal of static typing is not to have untyped values. We need to define some types for our attributes. For instance, we can declare the name as a string and the age as an integer. The initializer will accept these two parametersā€”one as a string and the other as an integer.
00:06:24 Now, if I try to instantiate this user class with two string arguments, it will give me an error, indicating that the string type is not acceptable because the expected argument type is integer.
00:06:43 These kinds of errors can be caught early when we annotate our code with RBS.
00:07:16 Let's see what happens with dynamic attributes. If we are dynamically defining getters and setters for attributes like name, age, and gender, then using RB option will only parse the Ruby code syntactically.
00:07:28 However, it will not identify all methods in the class correctly. To resolve this issue, we can use the runtime option.
00:07:55 The runtime approach allows us to specify the path of the code where the class is defined, and we can generate the corresponding RBS code.
00:08:16 Now, let's look into different types of functions and the corresponding RBS signatures we can write. For example, we have a method called 'greet' that accepts an optional string argument. In RBS, we use a question mark to define that this value can be nil. This way, we can call the greet function without any argument.
00:09:19 Next, we have an example function that can accept a variable number of arguments using the splat operator. The first argument is fixed, while the rest can be an indefinite number of additional arguments.
00:09:36 The return type in such cases can be defined as Boolean if the function checks if the name ends with a particular value.
00:09:55 In another example, we have a 'multiply' function in a class that accepts two arguments: the first and a multiplier. Depending on the types of parameters (string or integer), it executes accordingly.
00:10:30 RBS introduces union types where the parameter can be either a string or an integer. Optional attributes in RBS can also be referenced accordingly.
00:10:58 You can create functions with keyword arguments, allowing variable inputs with conditions for argument type, and specify their return type as either a string or an array of strings in RBS.
00:11:19 In cases of duck typing, if a class is defined with dynamic methods but lacks proper RBS definitions, we can use interfaces to manage expectations about methods that should be available in that class.
00:11:50 We can define an interface that includes methods like wheels, engine, and roof, which can be associated with a class adopting these methods.
00:12:05 Errors like type checking errors can be resolved using 'Steep,' a tool specifically meant for type checking RBS codes against Ruby codes.
00:12:40 Steep takes input of Ruby code and RBS signatures, verifying if the Ruby code corresponds accurately with the defined structures in RBS.
00:13:10 Setting up Steep involves initial configurations just like RBS and defines where signatures will be found.
00:13:48 Steep is also capable of providing statistics regarding code coverage and can ensure that all annotations have been properly applied throughout our code.
00:14:15 When discussing Sorbet, the primary difference compared to RBS is that Sorbet is an annotation-based system, whereas RBS relies on separate signature files.
00:14:41 In Sorbet, for every function, we provide annotations that define the return types and expected parameters inline. This seamless integration allows easier understanding of function declarations.
00:15:15 Here, we can see how RBS and Sorbet code compare for the same functionalities. The RBS captures function signatures whereas Sorbet allows direct annotations.
00:15:50 Both systems offer some fascinating capabilities, but RBS currently has certain features under active development, aiming for enhanced flexibility in structuring code.
00:16:55 In conclusion, tools like Sorbet and RBS can significantly enhance type-checking in Ruby and improve code robustness, usability and provide newer ways to manage our code. Thank you!