RubyKaigi 2018

A practical type system for Ruby at Stripe

Slides: https://sorbet.run/talks/RubyKaigi2018/

At Stripe, we believe that a typesystem provides substantial benefits for a big codebase. They :

- are documentation that is always kept up-to-date;
- speed up the development loop via faster feedback from tooling;
- help discover corner cases that are not handled by the happy path;
- allow building tools that expose knowledge obtained through type-checking, such as "jump to definition".

We have built a type system that is currently being adopted by our Ruby code at Stripe. This typesystem can be adopted gradually with different teams and projects adopting it at a different pace. We support And and OrTypes as well as basic generics. Our type syntax that is backwards compatible with untyped ruby.

In this talk we describe our experience in developing and adopting a type system for our multi-million line ruby codebase. We will also discuss what future tools are made possible by having knowledge about types in the code base.

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

RubyKaigi 2018

00:00:00.030 Thank you very much, I appreciate Matz's talk this morning about naming a project being very important. Hopefully, you like ours. We're gonna start off with some introductions about ourselves.
00:00:05.790 I'm Paul Tarjan. I've been at Stripe for two and a half years, the entire time working on the developer productivity team. Before that, I was at Facebook for five years working on all sorts of different projects, most notably HHVM and the Hack language.
00:00:18.480 Hello everybody, I’m Dmitry. Before joining Stripe, I worked on the Scala compiler, and the next major version of the Scala compiler, Scala 4.3.0, will be based on my design. I'm Nelson, and I've been an engineer at Stripe for five years working on systems infrastructure and, more recently, developer productivity. Prior to that, I worked on hot patching the Linux kernel at a startup called Ksplice.
00:00:37.860 Alright, thank you, friends! Hopefully you enjoy our talk. Number one, what is Stripe? Some of you may not have heard of us; hopefully many of you have. We are a platform that developers use to accept payments. We operate in 25 different countries, and a hundred thousand businesses worldwide use us. An interesting fact is that about 60% of people in the United States have used our services in the last year for a transaction.
00:01:04.470 I suggest if you are running an internet business, please check out Stripe. It will be an enjoyable experience if I can have my way. We do have a Tokyo office, so there’s something nearby if you're interested. Many of the folks from the Tokyo office are in the audience, so can we wave to these fine folks? If you are interested in anything Stripe Tokyo, here is the obligatory link that you have to put on any conference slide: stripes.com/jobs.
00:01:31.680 We mentioned that we are under an umbrella team called developer productivity. Our company is very dedicated to this. We believe very strongly that putting effort into developer productivity yields significant rewards in the end.
00:01:58.110 Our team consists of many sub-teams within it, focusing on all parts of the pipeline from the moment the code needs to be written to when the code is deployed. We specifically focus on developing language tools to enhance Ruby, at least at Stripe, and hopefully better for you.
00:02:24.900 To give you some background on how we use Ruby: we are a very large Ruby shop with millions of lines of code all written in Ruby. It is our primary programming language for developing our products as well as many of our internal tools. We do not use Rails; this is an important distinction that you will see later on regarding our type checker.
00:03:10.680 We do have an enforced subset of Ruby, thanks to the work by RuboCop. We appreciate it highly and hopefully we’ve contributed back a lot to your project. Internally, we have over 300 rules for maintaining code quality, both syntactic and semantic.
00:03:37.170 Most of our code is in a monorepo. This is intentional; it isn’t an accident of history. We are a microservices architecture, so we have very large services that encapsulate a lot of functionalities, along with a few satellite microservices around their periphery.
00:04:07.440 Lastly, new code is being written fresh; we aren’t in the process of switching to microservices or anything like that. We embrace this architecture and we are deploying new code into most of the new microservices. This is the shape of Ruby at Stripe.
00:04:51.180 Here's our programming language breakdown to give you an idea of what we're using. Ruby remains our number one language, and almost every engineer joins the company trained in Ruby. Our second most popular language is JavaScript, our third is HTML, which is also a very excellent language.
00:05:10.220 We have millions of lines of code, hundreds of engineers, and we are growing quickly with thousands of commits a day. Our code team has their hands full with maintaining our Git repository. We are not the first type checker for Ruby; we are standing on the shoulders of giants, so we appreciate all the work that has come before us.
00:06:07.220 We hope to collaborate with others in the community, including Diamondback, Ruby, P, Ruby, Ruby Dust, RTC, and RD Li, which have been around for years. Jeff Foster and his team at the University of Maryland have been working on an excellent project, and we appreciate them for their runtime annotations used in the standard library, even within our type checker.
00:06:41.820 There is an unreleased GitHub experiment by our friend Charlie Somber at GitHub, who has graciously lent us some of his code for our parser. There are also presentations tomorrow, as Matz mentioned in the keynote, and I encourage you to speak to our friends.
00:07:01.500 Lastly, there was a conference talk given last year at RubyKaigi about contracts by JetBrains, highlighting a lot of interest in this field. We appreciate that and are eager to utilize past work.
00:07:36.900 The elephant in the room is about whether we will be open source. Eventually, we will open source our project. The goal is not to build something in isolation just for ourselves; we aim to help the community and build out a type checker.
00:08:05.100 Currently, we have limited resources since we only have three people working on this. Our plan is to prove it out internally first before we launch it to everyone. However, we encourage you to reach out to us with any ideas, inspiration, or issues. Your feedback will help shape the way we refine our release.
00:08:35.700 I do not want to create vaporware; merely discussing something that does not exist is not as engaging as showcasing a working product. If I switch tabs here, you’ll see a URL. I’ve worked on the Hack language, a PHP derivative, and combining a string with an integer is quite standard in PHP, which is not possible in Ruby.
00:09:17.160 You'll notice we made a type check error here: an expression passed in to the argument zero for the method plus does not match the expected type for 'one plus two'. That demonstrates a mismatch in expected input.
00:09:52.920 So how do we add things? Well, we can call a method; oh, that’s a syntax error! What method can we call on an integer? It's not a valid method! Right, if I cast my integer into a string now, I can combine it with another string, which is how we solve this issue.
00:10:36.630 Here is our type checker running in the browser. As I mentioned, it is not vaporware. Feel free to visit sorbet.run on your device of choice or use a QR code reader to try it out. We will leave it live even after this.
00:11:05.760 At this point, no one will listen to the rest of the talk. With that, let me hand it over to my colleague, Dmitry.
00:11:24.420 Hello everybody! I'm starting Bardwish. I'm going to be competing with you trying the editor. When we started designing a type system for Ruby, we noticed that Ruby is quite different from many other languages that already have types like Java or C++. It’s better in many regards.
00:11:50.580 Thus, we began with some key design principles. Since we are a large company with many developers, we needed to make the type system explicit and allow it to serve as maintained documentation. We wanted our developers to feel rewarded for the time they put into writing type signatures.
00:12:13.390 The type system needed to be useful and not burdensome. We aimed to create a type system that feels rewarding, different from Java, where developers wish to write type signatures and see the benefits from doing so.
00:12:33.760 At the same time, we care a lot about the user experience. Even after type signatures are written, we want them to be understandable. We also want type errors to be easy to follow. Complex type systems often create convoluted error messages, leaving users with unhappy paths.
00:12:59.490 Next, since this is Ruby, we didn’t modify the syntax, the parser, or anything in Ruby itself. It runs on bare Ruby with no patches to MRI. This is important for us because we utilize a lot of libraries and tools that are integral to the Ruby community, and we want to maintain compatibility.
00:13:38.160 As Stripe continues to expand, our type checker system must also grow with it. Currently, it processes dozens of millions of lines of code and will need to handle even more soon. We want to allow relaxed and strict environments in our type system.
00:14:04.160 This flexibility helps with adoption since different teams have different schedules, allowing them to adopt the system at varying paces. Those are the high-level principles. Now, let me demonstrate one of the most important areas for a type checker: its messages.
00:14:46.830 Here's a demo of usage. If you use the type system correctly, it stays silent and simply returns zero. If you encounter an error, it will be explicit about what you did wrong. In particular, if you try to add a string to a symbol, it will inform you about the method you are calling and how it does not match the expectation.
00:15:07.950 The type checker will point out where the error occurred and even suggest corrections. This becomes very helpful if you have access to the repo and can easily navigate to the definition.
00:15:48.870 Being a type system for Ruby, we want to maintain the common ways Ruby code is written. For instance, when you access an element in an array, Java might return an element, but in Ruby, it could return nil if the index is out of range. Thus, it's typical in Ruby just to access something in the array.
00:16:14.700 In our analysis, we consider whether an object can be true or false. For example, if you test for not nil, the following lines will assume that the string is never nil. However, if you forget this check, an error will indicate that you are calling a method on a nil value, which doesn't exist.
00:17:03.540 It's crucial for us to keep our error messages short, concise, and useful. Additionally, we have other checks designed to uncover bugs, especially around common pitfalls related to variable declarations and types.
00:17:32.520 For example, in Ruby, you can define variables independently and declare them differently in conditional branches. However, if you're familiar with C, you may assume that a float near zero is false, which is not true in Ruby. Our type system will be able to identify this pitfall.
00:18:02.820 We can also model more complex types without needing extensive declarations. This can include an array of strings or integers, and when calling certain methods, we'll ensure the method being called exists for the actual types, helping to catch misuses.
00:18:27.750 Now let’s examine how to define types, which is crucial for library authors at Stripe. We’ve defined a DSL around Ruby, allowing you to write something called 'sig' before every method. Here, you specify the type of every argument.
00:18:49.200 For instance, when creating a charge, we define the necessary types for the amount being processed. This means that if you pass the arguments in the wrong order, the system will catch that mistake.
00:19:12.510 The static type checking helps us identify these issues early, while our existing system at Stripe also performs runtime checks to identify discrepancies that may occur.
00:19:35.820 We want developers to feel rewarded for every type they write. Therefore, we do not require them to repeat types when they are evident, such as when assigning an integer. However, if they wish to annotate it for consistency, they can use 'T.lat' to specify the expected type.
00:19:57.630 We want to allow developers to adopt this system at their own pace and afford flexibility in strictness levels. Without defining a strictness level, developers might encounter basic errors or typos, while higher levels permit thorough type checking.
00:20:22.970 The strongest level, type strong, mandates that everything be type-checked. It’s not common but useful for critical code segments where reliability is crucial. Our type system is designed to remain both simple and expressive.
00:20:50.070 Basic features include generics that are necessary for modeling structures like arrays, sets, and maps—where the declaration does not universally define all types. Additionally, we have complex generic methods that are key for precise type specifications in methods like 'array.map'.
00:21:24.540 Next, let me transition to Nelson, who will discuss our experience deploying this system and the feedback we’ve received from users.
00:21:54.860 The title of our talk has been a practical static type system for Ruby, and one of the exciting aspects of our type system is that we’ve been developing it while rolling it out to users, seeing how it operates at scale in a large code base.
00:22:18.060 Since last year, the runtime type system that Dmitry described has been deployed and users have had the opportunity to write their type signatures for about six months now. The static type checker is currently in an internal beta.
00:22:44.740 Currently, engineers at Stripe can opt-in to run the static type checker locally using a command line tool to see errors as they develop. Although not everyone uses it yet, the response has been fairly positive. To give you a sense of adoption, our internal engineers have authored around 3,000 type signatures across different methods.
00:23:18.370 They've opted in a total of around 150 or more files with type-checking annotations, which gives us an insight into how our type system is being utilized and its value.
00:23:55.300 In addition to those signatures, we can also infer about 240,000 signatures due to our support for Stripe's internal ORM. Therefore, we are able to extract types from our database objects.
00:24:31.690 Our type checker has also uncovered some latent bugs in our existing code. Despite having a robust culture of testing and code reviews, we still encountered some bugs that had lay dormant within the application.
00:24:59.410 Fortunately, none of them were critical, but they give a perspective on the bugs our tool is able to find and help eventually ensure their absence. The following are some examples from real bugs that emerged during the rollout.
00:25:40.300 The simplest ones are typos in error handling pathways. These pathways tend to get less thorough testing, so they often miss potential errors. For instance, an author wrote 'json.parse error' when the actual exception class should have been 'JSON::ParserError'. We flagged this error and suggested the correct constant.
00:26:11.470 Another case involved incorrectly calling 'ArgumentError' as a function instead of calling the constructor. In Python, this code would work, but it doesn't in Ruby. Again, our type checker alerted the developer.
00:26:36.740 Our nil-check support was critical in finding many real bugs. For example, the type checker knows that a certain API call either returns a webhook endpoint or nil. If there's a missing nil check for the endpoint, it leads to errors.
00:26:59.640 By guiding the developer to ensure they handle the nil case, we've been able to improve overall application reliability. These types of issues are also identified before hitting production.
00:27:26.890 One common mistake involves accessing instance variables from a static method, which our type checker can identify. The type checker reported the access to an unassigned instance variable, ensuring the developer is aware of the issue.
00:27:40.380 In one instance, a developer misunderstood Ruby's case statement, using the logical `or` operator. One part of the logical expression repeatedly evaluated to `true`, causing the stray logical condition to be suppressed, leading the code into a logical error.
00:28:09.190 This situation went unnoticed because there weren’t tests set for that particular pathway within the utility script used at the time. Fortunately, the unreachable code detection helped uncover this oversight.
00:28:48.180 The first piece of feedback we received is people appreciated our beautiful error messages and the speed at which they were produced. Specific individuals remarked on the rapidity of the feedback they receive through our type checker.
00:29:23.780 Our tool is designed to be efficient, where we can type-check about 100,000 lines per second per CPU core. This means counting roughly a million lines of code could happen in about ten seconds, a significant advantage.
00:29:58.340 For perspective, Java compiles code at about 10,000 lines a second, while our testing environment runs around a thousand lines per second. We are striving to ensure swift interaction with our users.
00:30:23.110 Our implementation is done in C++, allowing us to avoid dependencies on a Ruby VM, making it a standalone program. We've also used a port of GitHub's excellent Ruby parser to facilitate our operations, allowing for significant time savings.
00:30:48.290 We run an extensive test suite that interacts with our type checker, ensuring reliability through consistent testing against our entire Stripe codebase at every pull request. The goal is to maintain confidence that pieces of code are robustly functioning together.
00:31:24.390 One trade-off exists when running outside a Ruby VM serves as a limitation since we don’t get support for various meta-programming capabilities for free. However, we have some patterns for handling core Ruby idioms along with our support for the ORM.
00:31:47.750 In summary, deploying this tool across a large existing codebase has been a fascinating process, with overwhelmingly positive reaction from users, showcasing the value and experience garnered from it. Now, let me pass it back to Paul for a closing.
00:32:13.640 Thank you, Nelson. First of all, I always like to document my presentations with a photo, so if everyone could wave excitedly... Thank you!
00:32:21.740 In conclusion, can you use it today? You probably already have, as I’ve noticed many people engaged with their laptops. Please continue to visit sorbet.run; we will keep it live after the conference.
00:32:47.130 We will be open-sourcing this project; however, due to limited resources, we want to ensure it is properly validated internally first. We want to avoid releasing vaporware or throwing code over the wall—it's far better to have a supportive rollout.
00:33:07.050 When we release it, we want to ensure community support is established. There will be a blog post, so please subscribe to our blog if you are active on RSS or hope to see it on Hacker News.
00:33:36.880 Lastly, please contact us via email if you'd like to be informed when we release it. We are looking for feedback from a broad range of individuals interested in scaling Ruby in large companies.
00:34:06.620 We're particularly interested in scenarios involving large codebases as well as those working on type checking. Please reach out—we'd like to collaborate and compare best practices.
00:34:36.430 Takeaways from today include: number one, we have a type checker that runs in your browser as well as elsewhere. It is fast and built thoughtfully, designed for day-to-day use.
00:34:57.380 Type signatures are meant to be useful and not burdensome, and this is an optional type checker that you can apply where needed. We look forward to open-sourcing this project, so hold us accountable.
00:35:14.280 With that, I’d like to thank the conference organizers for inviting us, a set of three people you have never heard of before, to come and talk to you about a type checker.
00:35:38.920 Thank you everyone for spending the past 40 minutes with us. And now, we have some time for Q&A, so if you have questions, please come to the microphone in front. Thank you!
00:35:47.890 Unfortunately, you are not all miked, so please come up to one of the two microphones on the left.
00:36:03.220 Thank you for your presentation. I have a question about the captured insults for those arguments. Should they be based on the class or the method?
00:36:36.140 We have a normative type system where we declare the shape of the methods based on the class. We believe the names are significant and if you cannot name what you are taking, you need to reconsider.
00:36:53.150 We do have interface support, so you can build out interfaces with modules along with type annotations.
00:37:07.310 During Matt's talk, one question about static typing was raised. He mentioned he doesn’t believe type annotations have a future in Ruby because type inference will get there in 10 to 20 years. What are your thoughts on that?
00:37:40.093 We believe the annotations are useful for the immediate future. We need these types now and can’t wait until 2040.
00:38:11.010 Thank you very much! Did you consider supporting the generation of annotations from Yard comments?
00:38:36.200 Yes, we already have a translator that can spit out interface files generated from Yard comments.
00:38:53.650 And what about dealing with duck typing, where methods accept objects that respond to certain methods?
00:39:00.768 Thank you for waiting! I think one of the hardest parts of introducing our type system is dealing with existing code that has no type annotations.
00:39:10.422 In a codebase filled with a high volume of libraries, we aim to compile a reflection-based signature generator that summarizes existing method signatures.
00:39:22.230 Unfortunately, I don’t have a number offhand for how many libraries we are using, but we are committed to documenting as we go.
00:39:38.239 Automating this process will allow for easier annotations as developers become familiar with type signatures.