BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Presentations Sorbet: Why and How We Built a Typechecker for Ruby

Sorbet: Why and How We Built a Typechecker for Ruby

Bookmarks
45:23

Summary

Dmitry Petrashko talks about Sorbet, a fast, powerful type checker designed for Ruby. At Stripe, they used Sorbet to drive code quality via measurable, concrete indicators. Petrashko covers why they started this project and what contributed to its success.

Bio

Dmitry Petrashko works on developer productivity at Stripe, making it easy to confidently write maintainable, fast, and reliable code by improving language, core abstractions, tools and educational materials.

About the conference

Software is changing the world. QCon empowers software development by facilitating the spread of knowledge and innovation in the developer community. A practitioner-driven conference, QCon is designed for technical team leads, architects, engineering directors, and project managers who influence innovation in their teams.

Transcript

Petrashko: This is a talk about Sorbet. Sorbet is a type system and a typechecker that was built at Stripe for Ruby. In this talk we'll discuss, why would we do that, and how did we do that.

First of all, who am I? I'm Dmitry [Petrashko]. I've earned a PhD in compiler construction working with Martin Odersky. My PhD thesis was Dotty, which is going to be called Scala 3, and now I'm working on developer tooling at Stripe, which includes everything from processes, core standard libraries, coding conventions, CI, everything. My work is to make sure that engineers at Stripe have the most productive years of their career there.

Here's an outline of this talk. During the talk please at any moment feel free to stop me and ask questions. I'm happy to answer all of them. We'll start with the context in which this is possible, which is Stripe.

About Stripe

Stripe is a platform that external developers use to accept payments. If you want to accept payments on the internet there are a lot of things that needs to be handled there to do it correctly. You want to make sure that you're compliant. You want to make sure that you're correctly doing things with credit cards [inaudible 00:01:10] them. That's hard, and companies use us to solve this problem for you.

We run in 32 countries and millions of businesses worldwide use us. There are billions of dollars processing every year through Stripe, and more than 80% of Americans adults bought something in Stripe in 2017. We have hundreds of people in 10 offices around the world. Customer reports more developer productivity after deploying Stripe, and as always, we're hiring.

Now, Ruby at Stripe - Ruby is the primary language used at Stripe. It's enforced subset of Ruby. You cannot use everything from Ruby. You can use the things that we consider to be sane in a company with a lot of engineers. We want to have our codebase be maintainable and uniform. We're not using Rails. We're using our own framework, our own [inaudible 00:01:57], our own [inaudible 00:01:58] layer, our own things that we believe work best for us.

Most of our product is a monorepo, and that's intentional. We believe that you get benefits by having a single versioning scheme while having clear notion of dependencies, and while being able to do all the changes in the same repo and the same PR. Majority of the code lives in 10 macroservices, and majority of new code goes into that. Now scale of engineering at Stripe - again, hundreds of engineers, thousands of commits per day, million lines of codes.

Problems Being Solved

In this environment, what was the problem that the type system was supposed to solve? Here's an email from Pre-Sorbet times at Stripe. This was a discussion about some specific user feature. As the discussion happened, the discussion was that the most common way it breaks is by seeing something called NoMethodError in production. NoMethodError is what happens in Ruby if you're trying to invoke a method on a class which doesn't have this method. For example, this will happen if you have the wrong class, not the one that you expected. You expected to have a string, you have an integer. Or, you mistype a method. This is the right class but you're just calling a method that doesn't exist in it. This was the most common kind of error seen in production.

The second one is NameErrors. This is slightly different but it's on the same vein. NameErrors is when you refer to classes, not to methods, while having type sometime. At the time those were the most common problems described in production, and in order to address them we went towards building a type system and deploying it at Stripe. Here are the design principles that we had behind it in order to make it work well specifically at Stripe.

The first one was explicitness. We want to write type notations. In fact, we see them beneficial. The reason for this being that they make code readable and predictable. As somebody who's not writing this code but rather reading this code, it makes it easier to understand what to expect of this method. What we mean by non-explicit here, the alternative that we could have chosen is to not write type notation but have a type system that's smart enough to figure it on its own and do it across methods. For example, [inaudible 00:04:18] does it, in particular for a [inaudible 00:04:20]. We've intentionally decided not to do this. Because in a company that's big where a majority of people read somebody else's code, it's beneficial to understand not what it does now but what it was expected to do, to have explicit intent and make sure that implementation fulfills this intent, rather than trying to back-solve the intent from implementation.

The second one is effectively a counterbalance to the previous one. While we want our users to write signatures for methods and describe boundaries this way, we don't want this to feel burdensome for this. In particular, when you're writing code inside the method we can actually figure out the majority of types for you. For example, here in the very first line, A is an integer. We don't want you to write it as an integer, we can figure it out on our own. Similarly on the second line it's a string, but here, if you want, we allow you to declare it as a string. We don't require you to do this, but if you want to be explicit about it you have an option to do this.

Next one is more of internal rather than user-visible design constraint. We want the type system to be simple but not simpler. What we mean by this is we want it to be as simple as possible while fulfilling the needs of Stripe. Here's a list of features in the order they were added to the type system. The reason for those was that every next feature was added not because we thought this feature is fancy or because we always wanted to implement this feature. It's because we saw real code with Stripe that's super common, not one method, hundreds of method - methods written by hundreds of engineers that need this thing to be modeled. Based on this we started iterating with a minimal set of features, adding features one by one, building the exact set that was needed to support Stripe codebase that pre-existed before that time.

Then the next requirement was we didn't build the type system for Stripe at the time. We built a type system that we wanted to live for a long time and continue delivering impact, which means we want to make decisions that scale with size of engineering, both size of engineer of our users and size of internal team who builds the typechecker. We also wanted to make sure we can address the many needs of different users of Stripe. Different people need different amount of rigor. Somebody is working on experimental features and they have still no clue what it should do, and they don't know what design it have, which infrastructure it has, which structure it has. They want to go YOLO.

Somebody wants to make sure that this code is super rigid, doesn't have a single problem, and can be proven to be correct. In the real world you can make both of them perfectly happy. You can make both of them at least not be grumpy with you to make sure that you can both prove majority of the usefulness without being constrained of somebody who wants to go YOLO, and you can also allow some people some degree of going YOLO.

The next one is, we want to scale with codebase size. From our experience, the majority of the codebases of big companies, Stripe, Google, Facebook, grow non-linearly with time. There's an argument whether that's a cubic function, whether that's exponential function. In practice it's a factor that grows super fast. We need to make sure that the tools we're building can be fast and still provide pleasant experience for users, and it also allows our users to isolate the complexity of the codebase, where they will no longer need to know the entire codebase and understands actions at a distance. Rather this tool can be used to create those packages, so interfaces, those abstractions that will make it easier for users to reason about their code.

We also wanted to make sure that our project scales with time, and by this we mean if there are some decisions that can be much easier addressed early in the project, they would better address earlier rather than being postponed.

To give you some of the numbers for them, the most users want to assess here is performance. Our current performance is around 100k lines per second, per CPU core, and we scale linearly up to 64 cores [inaudible 00:08:30]. For comparison, to give some base line, if you compare us with javac, we're around 10x faster than javac per core, and we're around 100 times faster than existing tools that use Ruby, which is rubocop, which only does syntax checking. It doesn't do any kind of modeling semantically.

From our experience so far, today, our tool is the fastest way at Stripe, at Shopify, and Coinbase, and all those companies to get iterated feedback. We'll go into this deeper, but the short summary is, it's today integrated in IDE, and the current latency for response is milliseconds.

The next one is compatible with Ruby. We did not intent to deviate from Ruby, either semantics or syntax. We want to continue using the tools that Ruby has. We want to continue using the standard IDs. We want to continue getting the value and want to provide the value for other companies. The whole point here is to improve the existing Ruby codebase, rather than creating a new codebase that might have been following better rules. We want to be able to adopt it in existing codebase, adopt it incrementally, and thus, code as Ruby.

Going deeper into this one, we want to be able to adopt it gradually. What this means is different teams, different people will be adopting at a different pace. Thus, we introduce something called strictness levels. The basic level does basic checks, such as syntax-level errors, and for all the code in our codebase, even if you didn't describe it as strictness level we wanted to at least parse. The next one is typed: true, which enables basic type checking. It makes sure that for methods for which you have signature they're correctly using its arguments, and when you're calling other methods you're correctly using the results.

Typed: strict enforces that every existing method that you write in this code has a type signature. It effectively says that not only I want to check my bodies, I also want to make a promise to people who consume me that I'm going to fully annotate my entire interface. This means, as somebody who'll be using this, I'll get all the awesome features. I'll get jump to IDE, I have other completion. I have Jump to Definition, I have type at hover. This is the level that you want to use if you're providing the typed API for your users.

Finally, this is level strong. This level, this allows you to ever do something untyped. Every intermediary exception, not only your API, everything that you do inside your implementations has to be typed and has to be verified whether it runs correctly, statically. Having all those in place, we want to adopt Sorbet.

Adopting Sorbet at Stripe

As a team who's building this for internal usage our goal is not to just build the tool. It's to actually make sure that the tool is useful, which means make Stripe use it.

Here's a time of line of adoption. It took us 8 month to build the thing to the level that we believed it's good enough, it's good enough to start adopting it widely. Actually by that time those 8 month included pairing with two specific teams to make sure that their codebase can be typed. The first team was the one who we believed has the simplest codebase, and the second ones was the one that we believed had the hardest codebase. We first wanted to make sure we , handle simple cases because we're just starting, and then wanted to make sure we can handle the most complex ones. Then we spent 7 months rolling this out, and then from there we're working on editor and open-source tooling.

What does this provide us? I'll illustrate this on a bunch of examples of Ruby code, and I'll describe you what would happen before at Stripe and what would happen now. If you were to have a look at this Ruby code, if you carefully see, there's a typo. The Hello inside the main method is mistyped, one letter "L" is missing. If you were to run this you will see this when running this. In practice, if you didn't discover it yourself it will take you either time to test CI, which means 5 to 15 minutes to get feedback that you had a typo there, or maybe worse. You'll hear it later when you deploy the change when the deploy will start failing and your deploy will be rolled back, or even worse. It won't fail instantaneously but it will fail subtly in its own way and you'll get paged at 4 a.m. in production.

If you were to do this at Stripe now, you wouldn't be able to even [inaudible 00:13:02] this. You will get this error and it will tell you that there's no such thing as Helo. This is true for 100% of code at Stripe. One hundred percent of code at Stripe currently cannot have a typo in a class name.

The next one is the method names. Similarly, in this one somebody wanted to have a call method called greeting, but the call side forgot that the name is [inaudible 00:13:26] and they just called greet for brevity. Similarly, in order to [inaudible 00:13:30] this error in base line Ruby and run this code, and you'll see this error. Similarly, unless you found this error yourself you'll find it either in tests, or in deploy, or worse, in production. Now, with our thing, you can find it statically. You add the type notation typed: true, which means we'll start type checking your code, and then we'll be able to tell you that the method greet doesn't exist. I'm actually not showing the entire error message. The error messages actually include suggestions, "What did you mean?" They didn't fit in the slides but we're also trying to be helpful in suggesting what was the thing that you wanted to do it, and you actually can pass us a flag to make us auto fix it.

This is true for 85% of code at Stripe today. You may ask why, and for remaining 15% of the code it takes effort to do this. For a majority of the code that we migrated so far, we migrated through automatic migrations, where we can go and fix [inaudible 00:14:34] bugs through automatic refactoring tools. We're a small team. When we started we were three people and we did the majority of migration ourselves. They built in tool that automatically do restructuring and handle error cases for you. The remaining 15% now are most bespoke things which are one-offs, and there's commonly one or two errors that prevent file from being [inaudible 00:14:57].

Increasingly this is code that when team goes and touches it they will see the majority of features and their ID don't work, because they only work in type files. Then they have a [inaudible 00:15:09] in front of them to get it to type, and majority of typing that has been happening over the last year is driven by users. I work on this file, I want the features that I'm used to working, so I'm just going to go and type it.

Yes, this is more than just errors. It allows you to express intent, and expressing intent is super useful for when you have a big organization and you have a question of, you have a problem. Is this one team who doesn't understand how to use the API that another team provides? Or is the team that provides the API who didn't handle the use case that the user expected to be handled?

Here's an example of this. You have a method do_thing, very descriptive name. Can you pass string to it? Maybe. What happens if you pass nil to it, which is Ruby speak for "null" in Java? Maybe it works. How would you expect it to work? Should it handle it? Who should handle it? Having type signatures changed the way culture and collaboration between teams works. Now you explicitly describe your intent, and you know that the first one is ok. The method is expected to take a string. The second one does not. This is a good illustration of another thing, which is we didn't want to postpone hard decision. One of the hard decisions that a lot of type systems postpone is whether nils inhabit every type. For example, in Java nil is a varied string, and thus, you can have errors in production when you pass nil. Kotlin is trying to fix it after the fact, and Scala 3 will be trying to fix this after the fact, but it's taking years for them. For us, from start we had this figured out, and our engineers don't know that this could have been problem that could have been [inaudible 00:16:54] them and paging them from production at 4 a.m.

Now, recap. What have we achieved? In 100% of files we can catch uninitialized constants. In 85% of files we can catch NoMethodErrors, and this is one more metric. In 75% of files, 75% of call sites, we know the specific method that you're calling. While this is not necessarily useful for type checking, this is the metric which tells you in how many locations can we do auto complete? This tells you how many locations can you hover over it and will you show documentation about what's called there? It would show you which types have been evaluated there. This is the metric that tells you that how useful this tool is as somebody who you're iteratively talking to as part of your development.

It's like somebody who knows your codebase really well is able to answer every question about it in milliseconds. Rather than you taking time, going too deep into a thing, trying to figure out whether it handles something, or where it's asking a team whether it's support to handle it, or where it's having this team be in a different continent in a different time zone, and you need to wait 24 hours for it.

What Our Users Say

Did I say we're in 10 offices? All of this were just numbers, and they're numbers coming from somebody who chose this number, so they're metric. They might be representative, maybe I'm just trying to say that this is an awesome metric but people actually hate it.

Here are some screenshots of what users say. This is an example of a person who is describing that they would use the type system to annotate the code that they rarely use. They touch it once every year or so. They always forget what it does. Going once there and describing what it was supposed to do makes it easier for them to go back to this code in a few years. Here a user is describing that they like the pair programming experience that they get from the tool. It's super useful that we can work on programs that are incomplete, programs that don't parse, programs that are still being written, which means developers can get early feedback and can adapt their design to be better modular, better readable, better in almost every way.

In the past, if somebody was to write a method that used to take either a string, or an integer, or an array, or a Boolean, now if they were to write this as an explicit signature they would feel bad. Before this they could have thought, implicitly, I guess, maybe it will work, but now they have to actually write this down, think about this, and now when they think about this, they're starting to question whether that's the right desire. Similarly, as part of code review, now code reviewers see this, and if somebody is trying to introduce the method that serves 55 purposes and takes 55 different kinds of arguments in specific combinations, it's now explicit and people will [inaudible 00:19:54].

This is a message from an early user who found an undocumented flag to enable hover. We're doing internal feature releases where some features for ID are enabled for some users, some features are not, but by passing magical flags you can enable them for themselves. We grew a group of users that was back solving them in order to enable features. They also broke our metrics because some of those features are not enabled because they're not fully stable, because they crash. At some moment we became so useful that people would prefer to enable features crash once every day, because they're so useful despite crashing from time to time. Since then we stopped having those flags, because we want to make sure everything doesn't crash. We're pausing it, we're having metrics around it.

Being written in C++, crashes are scary because everything that you have a crash can [inaudible 00:20:55] something like this. We don't intend to have crashes.

Finally, it's fast. We have million lines of code and the tool completes in seconds. When starting from scratch and when you use in incremental mode in ID, it's milliseconds. Depending what you do it can be single-digit milliseconds, can be a few more.

What We Learned

Ok, so what have we learned? Sorbet is a powerful tool that feeds many needs. We have different users. Some of them are building new projects and new products, and they want to move fast and not yet figured out what their things are supposed to do, and thus, not care if they're broken. We have some other users that are literally moving money, literally moving big piles of money, and they want to make sure that they move the money correctly, because otherwise, well, big money is at stake.

People love using Sorbet. Originally we had people who were pushing back. Originally we had people who said something along the lines that Sorbet is stopping them from doing the thing that they want. This commonly comes from two reasons. One is, a lot of people came from teams that, in previous companies, didn't grow as fast as we do, and they had a team of five people, and they had a team of the same five people over four years. Thus, making it easy to onboard new engineers, having code that's easier to understand was not something that's a big constraint of theirs. Stripe is growing fast, and thus, for us it's super important to make sure people can easily understand the code. A majority of the code that Stripe is writing today will be maintained by more people as we grow.

Second reason is that at the time they were complaining about this, just to be honest, Sorbet only had a stake. We were saying, "You're wrong, your program is wrong. Don't do this." Now, we also have candies. We can give you information about, what does your code do? We can do refactoring for you. We can do jump to definition. In previous time if you had a method with a common name and you wanted to figure out which method with this name specifically are you calling, and there are thousands methods with the same name in the codebase. You need to figure out which of the hundred is actually here. This was hard. Now, you can just Command-click and you're there.

The final part, which is more of internal part, this could not have been possible without automating the migration. The majority of typing, the majority of making the code actually follow the rules, fixing the common code patterns, fixing up [inaudible 00:23:39] null checks, or at least making them explicit was made by our team. It wouldn't have been possible to stop product development in Stripe and ask everybody, "Please go type your code or rewrite code to do something about this." The biggest value proposition is this happened underneath people, underneath the majority of developers without them needing to do much. They were doing feature development, and in a year later it became much easier to do feature development. It became much easier to maintain their code, so they love it.

This was the majority of their love. The rollout was the most important part. In retrospective, it was the super right call to have this very same team who developed the tool do the rollout because it allowed us to understand the user cases. It allowed us to figure out what should actually be allowed and prohibited, and which features do we need?

Now, we're in editor and OSS tooling mode. We have integrations with VS Code. We implement something called language [inaudible 00:24:40] protocol, the referenced implementation for VS Code, but there are other implementations that work for it. People at Stripe have implemented implementations for VI. There is also an [inaudible 00:24:52] implementation lurking around. Yes, it seems to be doing well. We have errors, we have hover. We have Go to Definition. We have auto complete in documentation. As you're writing the method you can see which methods have this name. You can also see the documentation for them and figure out which ones did you want.

Try It Yourself!

Also, in case you were seeing from the previous talk here that was about WebAssembly, this thing is written in C++. We compile it to WebAssembly, it runs in browser. If you go to sorbet.run you can actually try it. Unfortunately, with the way how it works it doesn't show all the features in your phone. Specifically, auto complete will not work because we're actually using a small version of VS Code there, and Microsoft did not intend to explore cover use cases of running VS Code on the phone.

If you go there from your VS Code you'll have the basic experience, and if you go from your computer you'll get auto complete, you'll get Jump to Definition. You can also Jump to Definition to Standard Library. You can see which method exists on standard Java, standard Ruby things. It's the way how a lot of people now prototype small things, because it allows them to [inaudible 00:26:02]. Increasingly, we see people from both inside Stripe and outside Stripe use it as replacement for [inaudible 00:26:10]. You can explore this code, you can understand it better. It's easier to write than [inaudible 00:26:15] because it's auto complete.

State of Sorbet

What's the current state of it? We're collaborating closely with the Ruby core team. Ruby 3 will have types. We're settling out the details about what's going to be the syntax. Syntax might be slightly different, but Ruby 3 will have types, and that's awesome.

It's an open source. We will open source it after having extensive private beta, with more than 40 companies adopting it. At this moment we know of a lot of big and small companies using it, both internally and for their recruitment. It provides better user experience and developers want to be productive, so having this is in our codebase you can use Sorbet. Sorbet is trendy, it's a good way to get people to enjoy and intend to work on your codebase.

You can check it out. The common companies who have blogged about it include Coinbase, Shopify, Heroku, [inaudible 00:27:17], and Ruby. Just to go into closing, Sorbet has moved errors discovery from test or production to development, which makes for much faster iteration tool for developers. It's open sourced. You can use it. It works as common Ruby code. I started by saying that we don't use Rails. Majority of [inaudible 00:27:38] does use Rails, and that's why I'm making so much emphasis on that other companies also do this. Because they do use Rails and they made it work for them. Specifically, CZI, Chan Zuckerberg Initiative has created a huge project called sorbet-rails, which adds enough things as extensions to Sorbet to support Rail's codebases and all the people using those. Docs are live at sorbet.org.

Questions and Answers

Participant 1: Ruby has a lot of really dynamic features, like whether I can re-open a class wherever I want. I can do instance_eval, OpenStruct, MethodMissing, just to name a couple. How does Sorbet deal with that?

Petrashko: The question was, Ruby has a lot of dynamic features, including being able to re-open a class, which is Ruby speak to define new methods into existing class, or define new interfaces as class, now implements just by the previous definition of it not saying it does, or being able to change the scope so that you're evaluating something with this pointer, which Ruby's spoken [inaudible 00:29:03] which is called class_eval and instance_eval. Those are very dynamic features, very [inaudible 00:29:07] programming features, and do we, and how do we support them?

The answer is two-fold. First of all, there is always something called T.unsafe. There is always a way for you to say, "I am intentionally doing something that you don't know what it is. I know that it's right." There is a backdoor that you can always open. Sometimes it's the right tool, sometimes it's not. For every feature, the question is, "Do we want to support it and make it official? Or, do we want to effectively say that this is an unsafe feature you can still continue using this, but we believe that there are better practice around this."

For class_eval and instance_eval, we consider it's unsafe. For reopening classes, we consider it safe. Sorbet natively supports finding all definitions that reopen a class, and being able to find all interfaces from them in other definitions. It's similarly from experience of deploying this at Stripe and in other companies. We believe we found a balance where some of the features are natively supported. Some of the features can be supported via backdoors.

Participant 1: One other question. Do you enable Sorbet in your test suite?

Petrashko: Sorbet has two components. I didn't go deep into this in slides, but then I dive deeper into questions. Sorbet has runtime components and static components. Static component is static typechecker. It runs as a concurrent job in our CI, and if your code doesn't type check it can't be merged. In tests we have also runtime components to verify whether your types are correct in runtime, and we run it in both CI, and production, and development, everywhere.

Participant 1: I meant to say, do you check the types of your tests?

Petrashko: The question is funny, because we didn't intend to. We grew experience from Facebook and Dropbox, who did not type check their test. Our users went to type check those tests for themselves. Our team, the majority of the [inaudible 00:31:12] were saying you should go and do this yourself for your users. Our users at some point found it so useful that they wanted the same features to work with tests, and they made it work. Some tests do. We still don't officially require it, but nor do we encourage or prohibit it. The thing about tests is we find the tests are typically more fragile, and thus, sometimes type system is not the thing that you want. In particular, if you like [inaudible 00:31:38] type system doesn't want to model this.

In particular, because our type system models what happens in production, and sometimes tests change the thing to not do the thing that production does in incompatible way. It's increasingly rare, but existing tests do this. Thus, our type system is the thing that most closely mimics production, and some tests don't want it to, so those tests are not accurate.

Participant 2: Does Sorbet have the interface where you might have three or four methods and you want a class to implement to [inaudible 00:32:18]? You might have multiple classes [inaudible 00:32:20]?

Petrashko: The question was, does Sorbet have interfaces, as in will you be able to say that this is some structure that you want multiple classes to implement, and thus, follow? Ruby has something called modules which we use as interfaces. We extended this to define notion of abstract methods. You can say that this is a module which has a method, and this method we describe as abstract in the signature. We will prohibit classes from being instantiated both in runtime and statically, unless they implement all their abstract methods. This effectively allowed us to have interfaces. We had this pretty early. More recently around four months ago we implemented the notion of sealed also, where not only can you define interface. You can also define interface that knows every class and implements it.

When you pattern match over it, we can do exhaustiveness checking. They do handle all of it. Yes, interfaces are super awesome. We're using them a lot. They're a great feature.

Participant 3: I may have missed this earlier. Is the typechecker implemented in Ruby itself?

Petrashko: The answer is no. The runtime type system is a Ruby library. It works on base line normal Ruby. It doesn't need any patches. You can use it in any Ruby codebase. The static type system is a separate program that's written in C++. The reason it being written in C++ is from my prior experience working in compilers. I believed that the thing that defines performance of compilers is good work on memory locality.

Compilers effectively have a bunch of core data structures that are huge hash maps, and the thing that will define whether you're fast or not is whether you can quickly find stuff there. Thus, the thing that's important for you is not your CPU performance, is not how many threads you have, it's how much memory you can read through your CPU, and how much memory do you waste reading through CPU? We chose a language which allows us to control from memory layout. The original team all knew C++. A lot of existing typecheckers and compilers have built a lot of tools for them, and we just built on them. In retrospect, we believe this was the right choice.

It also allowed us to compile to WebAssembly, and they have a very nice website, but at the time we didn't consider this as part of the choice. The website was built on a plane to Japan, and it's awesome, so it was super easy.

Participant 4: How does this work with existing Ruby gems? Is there a mechanism to provide external [inaudible 00:35:04]?

Petrashko: The question was, with Ruby having libraries, the way Ruby calls it, being gems, how do we type check something that calls methods into gems? The answer is, Stripe had a different solution for this, which is we actually [inaudible 00:35:21] majority of the things, but the other companies didn't. Shopify and Coinbase independently built a way to write an RBI as an interface file for a gem. At this moment I believe majority of people in open source, including Shopify, are converging on using the one implemented by Coinbase. You can put it into a gem and it will generate you a type signature for it.

Majority of the times they're going to be just untyped because it can guess it, but the idea is that given a gem you will get the skeleton.

Then if you want to go [inaudible 00:35:57] types to some methods you can go do this manually, commit it to your code control, and upstream it to a common repository that's called Sorbet typed, where companies exchange those type of interfaces between each other. All of this is built by Coinbase. They did an awesome job.

Participant 4: Is it open source?

Petrashko: All of this is open source.

Participant 5: Are there any advantages to using this for a new project over using a type language?

Petrashko: It's a tricky question. The question is, do you want to have the same codebase where people can be both strict and YOLO in different ends? This is the value proposition for new codebases, and somebody may want this, somebody may not. Some companies decide to, let's say, write a prototype in one language, say, Python, but write the actual implementation in other language, say, C++. The value proposition here is that you can go from YOLO to strict while staying in the same codebase.

There might be other reasons why as part of migration you may decide to choose a different language, let's say performance. At the current situation, the value proposition basically is this. If you're starting a new project, which I don't think matters for a new project, but it's an interesting consideration if you intend to have a big company. I don't think anybody's in the situation thinking as far along, but to the best of my knowledge this is the fastest ID integration and the fastest typechecker that I know about for a practical language.

RubyMine, in our codebase, takes a few minutes to start, and double or triple-digit seconds to do Jump to Definition. RubyMind has pretty much the same performance for Ruby and Java here. They use the same infrastructure for them, so the question is probably if you're starting a small company you should include in your planning how are you going to work in terms of million lines of code? This is something that is also unique proposition for this project. At the same time, I feel like there are 10 companies in the world who care about it.

Participant 6: How did you get such fast performance?

Petrashko: I didn't talk about this on JVM Language Summit. It's available on the internet. The short summary is, the most important tool for you is memory locality. Knowing what you want to do can define data structures that work well memory locality-wise for specific transformations that are performance sensitive. At the same time, we want to encode enough extension points because some people will want you to extend [inaudible 00:39:14]. You want to make sure that when they extend [inaudible 00:39:16], your performance still stays good. It's a balance between having the things entirely locked down where performance matter, and doing very good data structures for it, and data structures designed around not the common things of CPU performance.

There are entire algorithms [inaudible 00:39:38] studied called external memory algorithms, and those are the algorithms where you'll be solving a problem, how do I store the petabyte of data given a single computer which has a gigabyte of memory? You effectively have the same question between your CPU caches and your memory. When you intend to read a single byte from your memory, you actually read an entire row from the actual RAM into your cache. This capacity, this throughput is the one you want to utilize fully. Short answer is, there is a set of algorithms which is external memory algorithms that if you started from there you'd be able to write software that uses caches effectively. With our contemporary hardware having many layers of caches, if you use them effectively you have 10x speed ups, 100x speed ups.

Participant 7: I was at RubyConf two years ago maybe and there was a couple talking about type checking Ruby. Do you feel this is becoming a standard that will be adopted [inaudible 00:40:52] industry-wide, or is it just you [inaudible 00:40:55] because you wrote it?

Petrashko: I'm presenting a talk in RubyConf in a week and there's a separate track on typing with me opening the track, and then Shopify presenting this, their experience. Then one of core Ruby [inaudible 00:41:23] presenting his tool that works for typing. I think this stands here as this is one of the tools that Ruby companies, Ruby communities can decide to use. In some cases, this is totally beneficial. In particular, we strongly believe that this is a beneficial tool in a codebase that's big where notion of interfaces is useful, being explicit is useful, and setting up explicit expectations is useful.

It's also super good when you want to discover it through the codebase. That said, this tool doesn't stop untyped Ruby from existing. There are use cases where companies benefit from having super magical DSL that just solves 90% of their business problem in this DSL, and getting rid of this DSL for sake of type checking or trying to support it is just not worth it. My belief is in many companies it's useful. I don't believe it's universally useful for everybody. There are some use cases where there are some things that Sorbet wouldn't like that might be better for you, and maybe you choose that one.

Participant 7: I was more asking about, type checking is good or not depending on the use case, but Sorbet specifically. It seems it's used by a lot of the big companies. Do you envision this solution to type checking becoming the standard, or are there other competing alternatives that you also think are really good?

Petrashko: For context, the other implementations of type checking for Ruby are RDL, which is [inaudible 00:43:05] of Jeff Foster in his lab for around 15 years. There is work by [inaudible 00:43:12] Ruby. It's a different person who just happens to have the same name. There has been a project by GitHub called TypedRuby, and there has been a project by IntelliJ folks. I don't think it had a public name.

The current state is RDL is suggesting to use us if you want something production ready. They're doing research, they're good. [inaudible 00:43:34] is suggesting to use us if you want something that's fast. I don't know what's happening with the IntelliJ one. I think their thing was more similar to feedback-driven profiling, where they're not statically doing it. Rather they're gathering feedback from the tests and crowdsourcing from everybody. I think they synergize with each other where you can use that tool to infer types for us, and vice versa.

The basic question is, the RDL is really good in the sense it has advanced language features that we don't. You can express complex types to do type-level computations. You can do proofs. You can have your types access your database and do logic based on that. The [inaudible 00:44:18] that comes is this type checking speed is around 80 lines per second. Sometimes this is a better tool, sometimes the other is a better tool. We're all working together. We're all part of Ruby Type's working group for Ruby 3. All of those people who are listed are in the same meeting every month, where depending what's the set of use cases that you want to handle, different one of them work better.

The experience so far is we're the only one that has IDE support. We're substantially faster than the others. That said, we're less expressive than the others. Now the question is, do you value having a smaller language but which is supported better, or do you want to be able to do type-level documentations? Sometimes not using Sorbet and using something else is a better answer for you.

 

See more presentations with transcripts

 

Recorded at:

Feb 27, 2020

BT