Checking Your Types: An Overview of Ruby's Type System

Summarized using AI

Checking Your Types: An Overview of Ruby's Type System

Sabrina Gannon • April 07, 2021 • online • Talk

The video titled "Checking Your Types: An Overview of Ruby's Type System" features Sabrina Gannon discussing the type system in Ruby, focusing on the new type-checking features introduced with Ruby 3. The talk begins with an introduction to type systems, emphasizing the differences between static and dynamic typing, as well as strong and weak typing. Gannon explains that Ruby is dynamically and strongly typed, meaning types are determined at runtime but errors will be raised for incompatible types.

Key points of the talk include:

  • Definition of Types: Types categorize expressions based on operational capabilities, drawing on mathematical foundations.
  • Typing Categories: An exploration of how languages differ in type handling through static vs. dynamic typing and strong vs. weak typing.
  • Conversion Methods: Discussion of implicit and explicit conversion methods in Ruby, highlighting the limited automatic conversions that Ruby performs.
  • Gradual Typing: Ruby 3 introduces new features like RBS (a DSL for Ruby types) and TypeProf (a tool for type analysis), allowing developers to gradually adopt typing in their code.
  • Error Prevention: The new type-checking tools help reduce error occurrences such as NoMethodErrors and ArgumentErrors by establishing clearer expectations for variables and method calls.
  • Type Checkers: Introduction to Sorbet and Steep, two type checkers that facilitate the integration of types into Ruby code. Gannon provides examples of using types to prevent future coding errors and improve maintainability.
  • Real-world Applications: Through examples, Gannon illustrates how adding type annotations can catch errors preemptively before runtime.

Conclusions drawn from the talk:
- The integration of type-checking tools promotes improved code quality and documentation, making it easier for developers to understand and maintain their code.
- The importance of community involvement in the development of type-checking tools in Ruby is emphasized, along with the ongoing need for adoption and feedback.
- Embracing typing leads to new opportunities for Ruby developers, fostering a culture of intentional coding and better practices.

Overall, Gannon advocates for the benefits of using types in Ruby, encouraging developers to explore and implement these new tools in their coding practices.

Checking Your Types: An Overview of Ruby's Type System
Sabrina Gannon • April 07, 2021 • online • Talk

Checking Your Types: An Overview of Ruby's Type System - Sabrina Gannon - rubyday 2021

Typechecking is an exciting feature that distinguishes Ruby 3 and beyond! In this talk we'll explore how types work in Ruby with the future of type checking in mind to deepen our understanding of the value of the new type checking tools and to delve deeper into Ruby types overall. Here's to getting to the root of understanding NoMethodError!

The ninth edition of the Italian Ruby conference, for the third time organised by GrUSP, took place online on April 7th 2021.

Speaker and details on https://2021.rubyday.it/

rubyday 2021

00:00:00.120 Oh my gosh.
00:00:33.899 Foreign.
00:00:48.320 Well, let's go into our next talk.
00:01:05.939 Good morning, everyone, and welcome. Sabrina, good morning!
00:01:13.080 Thank you! It's great to be here, and I appreciate you giving us one hour of your time today.
00:01:18.960 How are you?
00:01:24.299 I'm well, thank you. How about you?
00:01:30.720 Good, good. We're halfway through the event. Sadly, I wish this day could start all over. It's been great so far.
00:01:38.100 That's awesome! Let me introduce you to our attendees today.
00:01:44.280 So, what are we going to talk about today? Type checking.
00:01:51.000 Type checking is one of the exciting new features that Ruby 3 offers, as Matt also explained this morning.
00:01:56.820 Sabrina is here today to give us a talk about checking our types. So, how does that look, actually?
00:02:03.479 For those of us who started working with Ruby ages ago, we remember that there was no type checking.
00:02:09.539 So, this can be disorienting. Let's hand the stage over to you. Thank you so much!
00:02:22.860 Welcome to my talk on checking your types in Ruby. We're going to refresh ourselves on type systems, what they are and how we talk about them.
00:02:34.260 Next, we'll talk specifically about types in Ruby and then we'll dive into the exciting new features related to types in Ruby 3, along with a few popular type checkers.
00:02:40.260 Finally, we'll work through some examples of what working with types in Ruby can look like and how these tools can benefit us.
00:02:52.680 So what are types? Type systems are concepts we inherited from mathematics. Essentially, they're a way to categorize expressions based on operations that can be performed on those expressions.
00:02:59.400 For instance, take the number two in JavaScript. The operations that can be performed on a number entail mathematical operations, such as addition and subtraction.
00:03:09.959 When it comes to programming languages, we categorize our type systems into a few general categories.
00:03:16.980 The first one is static versus dynamic typing, which essentially asks the question: when does a programming language know which type it's working with?
00:03:22.019 Statically typed languages resolve their types before runtime, wanting to know the types of their expressions beforehand. While older languages required type declarations, many newer type systems use bottom-up type inference to determine their types.
00:03:36.780 If a statically typed language were throwing a party, they'd know ahead of time who was attending and likely have the right amount of snacks.
00:03:45.659 Conversely, dynamically typed languages resolve their types at runtime on the fly.
00:03:52.500 If a dynamically typed language were throwing a party, they'd only know which guests were coming when they showed up at the door. They might be more fun since you could bring whoever, but in code, the crash is a lot more literal and less enjoyable.
00:04:08.759 Now, let's talk about strong versus weak typing. I feel the need to put a warning here that this is ambiguous territory.
00:04:14.759 These categories are not as binary as their naming implies. Type theory doesn't provide a definitive definition for either of these two categories, so how we use them can be very context-driven, with different people possibly having different definitions.
00:04:22.460 However, the distinctions are worth discussing, and by doing so, we might see how this line can be blurred.
00:04:33.180 Here's a historical definition of a strongly typed language: since newer languages tend to use type inference rather than type declarations, this definition doesn't precisely hold up for them.
00:04:54.900 So, what does a strongly typed language look like? Personally, I like thinking of this in an anthropomorphic way, wherein strongly typed languages have certain expectations about how their types will be used.
00:05:02.039 They'll throw an error if they're asked to perform an operation with a type that they don't understand.
00:05:09.060 For example, in Python, if you try to perform an operation with incompatible types, Python will raise an error saying it can't complete that operation.
00:05:16.380 Weakly typed languages, on the other hand, could also be referred to as loosely typed languages. They're more relaxed, less concerned about what is possible with types, and they still try their best to resolve whatever instructions you give them.
00:05:27.060 In JavaScript, for example, trying to combine a number with a string can lead to unexpected behavior, such as adding the number 2 to the string 'hello' yielding '2hello'.
00:05:35.580 Here's a rough chart showing where the languages we've seen so far fall roughly.
00:05:40.740 It also serves as proof that despite working for Dribble, I have not picked up any design skills yet.
00:05:50.760 So, my question is: where would Ruby land on this?
00:06:02.040 I imagine it to be somewhere around the dynamic strong quadrant, similar to where Python is.
00:06:08.440 Currently, Ruby is considered dynamically and strongly typed because it doesn't know its types until runtime.
00:06:15.660 Ruby is strongly typed in that it raises type errors and performs little implicit type conversion.
00:06:20.680 Wait a minute — what do we mean by little implicit type conversion?
00:06:28.540 There are three methods that the interpreter uses to convert between types in Ruby, and these have varying strictness levels.
00:06:35.680 Methods like `to_s` and `to_i` are explicit conversion methods, converting types that aren't closely related and generally not used automatically by the interpreter.
00:06:44.960 In this example, we see Ruby's birthday being converted from a time to a string representation.
00:06:50.200 This is possible because `to_s` is defined for time, but overall, the types aren't that related.
00:06:56.440 Implicit conversion methods like `to_str` and `to_int` tend to be defined for conversions between more similar types.
00:07:02.560 As their name suggests and in contrast to the explicit conversion methods, the interpreter will call them automatically in some cases.
00:07:08.800 This category of type conversions doesn't happen too frequently in strongly typed languages.
00:07:16.000 Another subtler kind of type conversion is numeric conversion.
00:07:23.480 An example of this would be adding an integer and a float together, which occurs when mathematical operations are called.
00:07:30.000 This is achieved by calling the `coerce` method on the object to return itself in a type that works for the operation being done, if possible.
00:07:36.360 In this case, we call the `coerce` method on 2 and pass in our float, allowing the addition to occur.
00:07:41.400 Now, the Ruby approach to adding types is gradual and opt-in.
00:07:48.360 You might feel that you're adding restrictions to your code and limiting the expressivity of the Ruby language, something many of us cherish.
00:07:54.000 So, this is a fair question: what's the point of types? As we've discussed, types help us categorize the values we pass around in our code.
00:08:06.000 As a result, our type tooling can infer and tell us more about our code before it even runs.
00:08:13.000 Since Ruby will raise errors if we don't use its types in a sensible way, we already have to care about maintaining valid types and values.
00:08:24.600 These tools will make it easier by giving our computers a way to check them for us ahead of time.
00:08:35.000 Let's talk about what Ruby 3 adds in terms of static type tools. Ruby 3 now includes RBS, which is a separate language for describing the types of a Ruby program.
00:08:56.000 It also includes a new tool called TypeProf, a type analysis tool included with Ruby 3, which reads non-annotated Ruby code and generates a prototype RBS type signature.
00:09:08.000 TypeProf is still very new and under active development and has a fun web version for experimentation.
00:09:16.000 Given our earlier discussion about the different type systems that are dynamic or static and strong versus weak, what does this mean for our new Ruby 3 world and the type checking tooling?
00:09:40.560 These tools introduce the idea of gradual typing to Ruby. We want to opt into which parts of our code we want tracked beforehand.
00:09:52.440 Introducing gradual type checking allows us to move some of our Ruby code into the lower quadrant of our type system chart.
00:10:09.600 There are common Ruby programming pitfalls we can avoid more easily now, such as NoMethodError and ArgumentError.
00:10:22.940 NoMethodErrors occur when we call a method on an object that doesn't properly respond to that method call.
00:10:38.939 This often happens because the receiver object is nil, and we're trying to call a method that is defined for the object we're expecting.
00:10:49.200 Gradual type checking tools can help reduce the frequency of NoMethodErrors by ensuring the expected type is what's being passed around in our code.
00:11:01.620 This guarantees that we always have receiver objects that meaningfully respond to our method calls.
00:11:16.200 Here we have an example where we're trying to call an instance method on the credit card class.
00:11:21.249 Ruby 3 also introduces RBS, a standalone language for describing types in Ruby programs.
00:11:25.320 Similar to TypeScript declaration files, RBS files are separate files that contain all type annotations for Ruby code.
00:11:37.680 The Ruby core team built this with the intention of providing the community with a solid, standardized foundation for developing type checkers and related tools.
00:11:47.160 You might wonder why we need a separate file; it sounds like more work. This decision ultimately comes down to preference.
00:12:00.720 Having the type declarations in a separate file allows our Ruby code to remain unchanged when we start using these tools.
00:12:08.520 Separate files provide valuable information about a program even before we look at the implementation code.
00:12:17.279 I've included this example from the Steep type checker to illustrate this point. Without opening the .rb file, we can see that we need a title and a year to initialize a new conference, with respective types of string and integer.
00:12:29.160 Now, let's talk about type checkers. Both of these tools preceded Ruby 3 and were part of the prior art informing Ruby 3's static type checking.
00:12:36.059 Sorbet is currently the most widely used static type checker for Ruby. It has its own type signature format called RBI, along with support for inline type annotations.
00:12:50.519 The Sorbet team is committed to supporting the RBS language, and the RBS gem ships with an RBI to RBS translator.
00:12:57.960 Steep is another type checker that utilizes RBS. Let's explore an example using Steep and types in a payment system.
00:13:05.360 Initially, everything works perfectly, and our system accepts either a credit card or a debit card as payment methods.
00:13:23.160 One day, we decide we want to let our users pay using an installment plan.
00:13:29.160 We write our integration and update the user class to return the new type of payment method.
00:13:42.960 Awesome! We make this change, get the necessary reviews, and ship it.
00:13:53.160 Suddenly, our observability metrics start showing numerous errors coming from the user profile page.
00:14:02.400 We quickly revert our changes and dive back into the class. The easy fix would be to add a new branch for installments.
00:14:15.360 However, I worry this is an easy mistake to make again in the future. Is there a way to catch this error ahead of time?
00:14:25.780 Yes, tests would have caught it, but I didn't even realize this component needed testing. There's a high chance we wouldn't know to write tests for any changes to our payment methods.
00:14:39.640 So let's try solving the problem with types first. I'm going to start adding types to the user class.
00:14:46.300 I'll implement types for the existing cases — debit card and credit card first — and ensure the type checker raises an error before I add the new installment payment method.
00:14:57.920 While writing the user type annotations, I need to consider what type this should return.
00:15:06.880 I could create a new superclass for all payment methods, but that would add another runtime error risk.
00:15:13.600 Anyone adding a new payment method might forget to implement this superclass, highlighting the same problem in a different spot.
00:15:23.600 Instead, I think I will use an interface to ensure I always provide my own implementation.
00:15:29.560 I'll implement this for all current payment methods, enabling me to eliminate the else fallback in the payment method presenter.
00:15:36.480 Nice! Now, I'll run my type checker.
00:15:41.560 I get a complaint that the payment method presenter claims to return a string but actually returns either a string or nil.
00:15:49.560 This seems odd, so I'll double-check that I didn't leave a faulty branch in my case statement.
00:15:55.600 However, it doesn't return nil as an option in my case statement. All the implementers of payment method are listed there.
00:16:05.600 Why doesn't Steep realize that?
00:16:16.760 It turns out this is an optimization in many type checkers; since any object could implement the interface, Steep needs to load them all to check if every case has been handled.
00:16:27.080 Is there a different way to do this? Yes, we can use Union types.
00:16:35.040 Union types are powerful; they allow us to create a subset of types.
00:16:42.160 For example, the type payment method can be either debit card or credit card.
00:16:47.580 When we make this change, the list of types becomes closed rather than open to any possible future implementers, allowing Steep to verify all cases are handled.
00:16:56.160 When we run Steep check, it passes.
00:17:03.000 Great! Now, I'll add in our new case for installments, which is another valid type for payment method.
00:17:10.240 Our original case statement will not include installments.
00:17:16.880 When we run it through Steep check, we'll receive an error, which goes away when we ensure the installments case is handled.
00:17:22.480 Now we can rest assured that this type of error won't happen again.
00:17:34.400 An added bonus is that when our future selves, or someone new, return to this code after some time, the RBS file will serve as validated documentation, helping them understand our code far better.
00:17:45.800 Let's discuss how to reduce the frequency of NoMethodErrors in our code.
00:17:54.160 We'll look at the Addressable gem, which is popular for parsing URIs, and examine its query_values method, which returns a hash of the query parameters.
00:18:06.840 Integrating this into our code reveals that we receive many NoMethod errors when attempting to get the version out of our parsed URI.
00:18:17.520 It turns out that query_values doesn't always return a hash as we assumed.
00:18:24.080 It can return nil if the URI lacks any query values or a question mark at the end.
00:18:32.560 Let's discuss how to prevent this situation using types.
00:18:36.680 I will create a class called BadAddressable that implements a simplified version of what we expect query_values to do.
00:18:44.160 I'll initialize it with a URI that doesn't have any query parameters and test that it generates the expected value.
00:18:53.600 I'll also add an RBS file with type annotations for BadAddressable, so we see that it initializes with a string question mark, which means it's an optional argument.
00:19:02.680 Query_values will return a hash with string keys and string values, along with a question mark indicating it could also be nil.
00:19:11.800 Let's run Steep check on our files and see if types can provide any warnings about this nil behavior.
00:19:21.360 The output from Steep gives us an error message indicating our optional hash return type doesn't have the expected method.
00:19:29.120 It also shows us the offending line, addressing Steep's type checking sensibilities.
00:19:39.600 We see that we received a warning from Steep before running this code in production or any tests.
00:19:50.040 It's a valuable example of how we can prevent NoMethodErrors from impacting our production servers.
00:20:03.720 This isn't meant to be a call-out of the Addressable gem, but a real-life example of a common issue.
00:20:15.540 We've covered a lot today, including definitions of type systems, how Ruby uses types, and what's new in Ruby 3.
00:20:28.060 I believe these type checkers exemplify how the Ruby community and core team can collaborate and learn from one another, particularly regarding the introduction of static type checking tools.
00:20:42.440 The next stage of success for these tools relies heavily on adoption, contributions, and feedback to ensure the community's needs are continuously addressed.
00:20:52.400 The fundamental point of this talk is to show that types have always been there; these new tools provide Ruby developers more power to be intentional and expressive with our code for both computers and fellow humans.
00:21:06.040 Using these type checking tools helps ensure that errors only happen once by making our code smarter, offering valid documentation that cannot become outdated, and building a foundation for exciting new tools.
00:21:19.760 This includes IDE integrations, and so much more.
00:21:33.600 As we wrap up, I want to take a moment to thank my partner Sam for reviewing this talk and helping me learn about type theory.
00:21:42.960 I also want to thank the Fleur de Ruby, an online reading group that inspired me to propose this talk, and GrUSP for organizing this event.
00:21:57.120 Thank you very much for watching this talk, and let's go forth and build with types, Ruby devs!
00:22:18.800 I really want to thank you for—
00:22:27.040 Welcome back, Sabrina! Sorry, I was listening to your talk and also dealing with my neighbor's drilling.
00:22:32.800 I apologize for the noise. Thank you for your talk. I especially appreciated the first part—
00:22:39.640 the overview of the different ways we categorize types and languages based on how they use them.
00:22:45.240 That was incredibly useful and informative.
00:22:51.040 Thank you! It was a lot of fun to write. This was my first talk, so I appreciate everyone taking the time to listen.
00:22:56.960 You did an amazing job, so keep doing this!
00:23:01.000 We do have a question already.
00:23:06.440 Machik is asking: could you relate RBS to TypeScript somehow? They seem to be similar; is that a wrong impression?
00:23:13.240 I don't think that's a wrong impression. However, I don't know TypeScript very well.
00:23:22.720 My understanding is that TypeScript is a superset of JavaScript, while RBS is a completely separate language from Ruby.
00:23:30.120 When you write RBS annotations, you're describing an existing Ruby program, focusing on inputs and outputs.
00:23:37.680 I think TypeScript allows you to have separate declaration files, similar to RBS files, but you also write TypeScript code.
00:23:46.840 Thank you for your question; it's a good one.
00:23:56.520 Another question came up from Luca: have you tried RBS and Steep in production code, and do you believe these tools are production-ready?
00:24:03.560 I have not tried Steep in production code yet. Writing this talk helped me start with the basics.
00:24:13.200 I was pretty skeptical about types in Ruby as a concept, so this has been a big learning experience for me.
00:24:24.560 I know Sorbet is being used in production in fairly large companies, and I've seen Sorbet used in previous jobs.
00:24:37.560 I would like to give Steep a try next, but I don't have specific examples of Steep being used in production right now.
00:24:48.520 You're getting cheers for adding the subtitles—thank you very much for that! It’s a nice addition to your already amazing talk.
00:25:03.680 I have a question as well: You mentioned being skeptical—are there situations where using Ruby types is not optimal?
00:25:09.720 Could you share an example?
00:25:15.800 I think the main issue arises when someone isn't being intentional about where and how they use types.
00:25:27.480 Some people can be skeptical about this feature, worrying about changes to Ruby’s freedom.
00:25:38.080 On the flip side, others might go all in too quickly without understanding best practices.
00:25:45.920 Finding a balance is key; having clear conversations with your team about using these tools will yield better results.
00:25:51.680 For example, I haven't used these tools in production yet, as we need to ensure team alignment.
00:25:58.440 Being strategic about how to introduce them to code will directly impact the benefits we gain.
00:26:09.840 That's a very good point! I'm thinking about this like integration tests—they're daunting yet necessary.
00:26:16.160 If you're using types for critical paths in your application, it makes a lot of sense.
00:26:25.720 At the same time, where things change often is where more flexibility is needed.
00:26:31.520 Another question emerged: what does the coding loop look like with RBS when developing new code?
00:26:37.440 Would you write type signatures by hand first and then implement the code?
00:26:47.840 That's a good question. A lot of the recent examples focus on integrating types into existing code.
00:26:55.920 For new code, it’s still something in exploration, and there's room to determine the best approach.
00:27:02.240 Personally, I think it’s beneficial to consider type signatures first, somewhat like test-driven development.
00:27:09.840 Determining functionality and what you're passing in/out will help during implementation.
00:27:20.520 I see many similarities with the Ruby and Rails community’s focus on testing as a form of safety.
00:27:30.720 Having types certainly covers aspects that tests would usually handle.
00:27:42.640 Are there any further comments or questions in the chat?
00:27:51.560 No more questions for now, but I have one final one for you.
00:27:57.600 While you were writing this talk, you were also studying law. What parallels did you see?
00:28:05.560 That’s interesting! I'm studying law part-time online.
00:28:11.680 The law is less fixed in stone than I initially expected. Case law heavily informs legislation.
00:28:16.640 This drew parallels to how we introduce types in Ruby; the process has been community-informed to this point.
00:28:24.800 There's plenty of flexibility ahead as we continue to develop and improve type checkers.
00:28:37.560 Thank you, Sabrina, for your time and for being here today!
Explore all talks recorded at rubyday 2021
+1