Soutaro Matsumoto

Ruby Programming with Type Checking

Last year, I had a presentation to introduce Steep, a type checker for Ruby. However, the implementation is so experimental that it cannot be used for production at all.
In this talk, I will report the nine months progress of the project and share the experience how the tool helps Ruby programming.

RubyKaigi 2018 https://rubykaigi.org/2018/presentations/soutaro

RubyKaigi 2018

00:00:00.030 Hello everyone. Last year, during the RubyKaigi in Hiroshima, I presented my idea on how we can introduce type checking to Ruby. At that time, I had a preliminary implementation, which was more of a proof of concept than a fully developed tool. This type checker is called Steep, and it serves as a gradual typing system for Ruby.
00:00:14.809 Steep allows you to write type annotations in Ruby, similar to comments, but currently, there is no type inference for certain aspects of the code. For the structure of modules, you must provide type annotations explicitly. However, it does allow for local type inference, meaning you can write Ruby programs without needing to annotate every single local variable. In terms of structure, the typing system in Steep primarily focuses on the types of objects. It incorporates features like union types and intersection types. For example, it supports subtyping relations between union types based on their structural properties. Unfortunately, the tool is still experimental.
00:01:06.300 Recently, I released a new version of Steep—version 0.3.0. Just a couple of days prior, I released 0.2.0. There have been numerous improvements over these past nine months. In this presentation, I want to explain some of these advancements. However, since my time is limited to 40 minutes, I'll focus more on demonstrating how Steep can assist in Ruby programming.
00:01:37.800 First, I'd like to show a quick comparison between Steep and Sorbet, another Ruby type checker. Both tools allow for local typing and share certain system features, such as generic union types. However, the main difference lies in their handling of subtyping: Steep utilizes structural subtyping, while Sorbet uses nominal subtyping. Nominal subtyping requires explicit naming of types, whereas structural subtyping assesses the structure of the types instead.
00:02:17.940 When defining types and method signatures in Steep, you can provide type annotations directly in your Ruby code. For instance, in a given Ruby file, you can use special method calls to indicate types for class variables. The type annotations are written in a way that allows the compiler to check them against the defined types.
00:02:50.700 A notable difference is that Steep performs fast type checking, while Sorbet is relatively slower. This performance difference can largely be attributed to the implementation language: Steep is written in Ruby, while Sorbet is implemented in C++. As a result, the overhead of checking subtyping relations structurally can impact performance.
00:03:32.230 I would now like to dive into a demonstration. Let's start with a very simple Ruby program that consists of only two classes. Initially, I'll show how Steep catches type errors in basic operations. For example, if we try to add a string to an integer, Steep will flag this as an objection since it mandates that only integers should be added.
00:04:13.380 Next, let’s complicate things a bit by creating classes and checking them with Steep. I’ll define a 'Presentation' class with attributes such as title and speaker, along with their respective types. The presentation will initialize with two keyword arguments—title and speaker, and it will have a method to print information about the presentation.
00:05:06.510 To clarify the structure, we can also define accessor methods for the title and speaker without having to annotate the instance variables every time. Notably, how instance variables are defined in the class’s signature matters; it is essential for correct API behavior, as these variables might be referenced elsewhere.
00:06:00.000 Following this, I’ll implement the 'Speaker' class, incorporating fields for name and GitHub URL, where the GitHub URL is optional. We'll ensure that the methods defined here properly handle the data types expected and return the correct values. This feedback from Steep helps ensure type correctness throughout our classes.
00:06:44.890 Now, moving onto testing, I will demonstrate how to utilize these classes in a broader context. We will be able to instantiate a presentation and check if the types are correct, revealing potential issues with our definitions and ensuring we align with the expectations set by Steep.
00:07:28.670 In conclusion, I have demonstrated the functionalities of type checking in small Ruby programs, showing how defining these signatures correctly can lead to better type safety. The steps involved include writing the signature, implementing the logic, and testing with Steep’s type checker to ensure accuracy continuously.
00:08:55.790 I have also delved into type checking real-world applications, specifically a Rails application, which poses unique challenges due to its heavy reliance on metaprogramming. I've provided insights into how to efficiently apply type definitions throughout the codebase and validate them with Steep, which requires careful consideration.
00:09:50.750 As I wrap up, I'd like to mention some roadblocks we faced during the type checking process. One significant challenge was the need for better support for type libraries, as many common types were not yet defined in Steep. As the tool evolves, incorporating more types and structure can enhance its utility.
00:10:50.000 Through this presentation, it’s clear that while we made considerable progress, the path ahead is still filled with opportunities to refine type inference and support for core Ruby features, ultimately aiming for a more robust and expressive type checking experience.
00:12:40.000 Thank you for your attention, and I would be happy to take any questions you might have regarding Steep or the implementation of type checking in Ruby.
00:13:35.000 Q&A: Audience member asks about the decision to drop automatic type inference. The speaker explains that while they initially aimed for automatic inference as seen in other languages, practicality and the differences in Ruby’s dynamic environment led them to favor explicit type declarations.
00:14:18.620 The speaker emphasizes the importance of explicit types to ensure correctness and programmer awareness, valuing the balanced approach of user-defined type annotations combined with the strengths of the type system.