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!