[HN Gopher] Sorbet: Stripe's Type Checker for Ruby ___________________________________________________________________ Sorbet: Stripe's Type Checker for Ruby Author : joeyespo Score : 93 points Date : 2022-03-28 18:04 UTC (1 days ago) (HTM) web link (stripe.com) (TXT) w3m dump (stripe.com) | areichert wrote: | I remember having mixed feelings about Sorbet when I first joined | Stripe in late 2018, but by the time I left, I found it | indispensable. Especially after the VS Code extension was | released internally... holy crap, that made such a huge | difference (vs having CI fail 20 mins after pushing up a PR | because you forgot to run the typechecker script ahead of time, | ugh). | | This article also made me laugh, because it reminded me of one of | my small pet peeves about the Ruby codebase at Stripe: the fact | that you would often find `merchant`, `account`, `invoice`, etc | used as method parameters that represented the _ID_ of the | resource rather than the resource itself. So Sorbet definitely | helped with that, but it also could've been nice to just write | `invoice_id` instead... :P | | Makes me nostalgic though, good times! | clintonb wrote: | I also joined Stripe in 2018, and thought Sorbet was a waste of | time. I quickly changed my mind when I realized how many | incidents it prevented. Now I want types for :allthethings! | itslennysfault wrote: | This is exactly how I felt when I was first forced to use | TypeScript instead of JavaScript, but I can't even tell you the | number of hours it has saved me. Now, years later, I can't | stand using regular JavaScript, and would never recommend it | for any project that will be beyond a toy. | hardwaresofton wrote: | Is there anything you can think to say to convince the old you? | I have a few friends who haven't yet seen the typing light. | | I also think Stripe's API (external) should not be moving ids | and objects. Given some payload in which 'account_id' is always | present and 'account' may be the object (using 'expand' IIRC?) | or not makes a lot more sense to me. | jez wrote: | My experience has been that the people opposed to types won't | be convinced to like types by anything you can say or have | them read. In all of the cases where I've seen Sorbet be | adopted, the process looked like this: | | 1. Ambitious team who wants types does work to get the | initial version passing in CI. Importantly, it's only | checking at `# typed: false`, which basically only checks for | missing constants and syntax errors. | | 2. That initial version sits silently in the codebase over a | period of days or weeks. If new errors are introduced, it | pings the enthusiastic Sorbet adoption team, they figure out | whether it caught a real bug or whether the tooling could be | improved. It does _not_ ping the unsuspecting user yet. | | 3. Repeat until the pings are only high-signal pings | | 4. Turn Sorbet on in enforcing mode in CI. It's still only | checking at `# typed: false` everywhere, but now individual | teams can start to put `# typed: true` or higher in the files | they care about. | | 5. Double check that at this point it's easy to configure | whatever editor(s) your team uses to have Sorbet in the | editor. Sorbet exposes an LSP server behind the `--lsp` flag, | and publishes a VS Code extension for people who want a one- | click solution. | | 6. Now the important part: show them how good Sorbet, don't | tell them. Fire up Sorbet on your codebase, delete something, | and watch as the error list populates instantly. Jump to | definition on a constant. Try autocompleting something. | | In my experience trying to bring static types to Ruby users, | seeing is really believing, and I've seen the same story play | out in just about every case. | | One final note: be supportive. Advertise one place for people | to ask questions and get quick responses. Admit that you will | likely be overworked for a bit until it takes off. But in the | long run as it spreads, other teammates will start to help | out with the evangelism as the benefits spread outward. | brandonbloom wrote: | > the fact that you would often find `merchant`, `account`, | `invoice`, etc used as method parameters that represented the | _ID_ of the resource rather than the resource itself | | I've encountered a few Rails projects in the wild that do this. | One solution is to make liberal use of the `to_param` method. | This method converts objects to strings that are intended for | use in URLs. Of particular note, it's the identity function for | strings and numbers, but returns `.id.to_s` for ActiveRecord | models. Using this within definitions makes your function | polymorphic for whether it accepts a model or an id. | | If you do this widely, would probably be best to monkey-patch | in your own `to_id` method. | hiphipjorge wrote: | We've been starting to use Sorbet at Figma and honestly it's been | pretty cool! Sorbet is definitely not as good at TypeScript | (yet?). It's more verbose, doesn't support things like recursive | types and records (shapes are experimental), and it doesn't | inspire the same confidence TS does but it's definitely worth it | to add it to your codebase if it's big enough! | | Also, it's fast! I'm in total agreement with the point made in | the article. That makes a huge difference in developer UX. | jez wrote: | I have some concrete ideas for how to fix shape types to make | them not incremental. Just a matter of finding the time to push | the prototype over the line, and do a migration on Stripe's | codebase to fix or silence the ensuing errors. It's one of the | most requested features for sure, and I think once we implement | it Sorbet will feel much better to use, especially in smaller | projects and scripts where you don't want to have to define | `T::Struct` for one-off data structures. | weaksauce wrote: | huh... didn't expect figma to be using any ruby. what do you | all use it for there? I'm mainly a ruby programmer lately but I | used figma for my last project design and it was really lovely | to use so good work! | flyingswift wrote: | Most of the backend is written with Ruby | ffggvv wrote: | kinda funny how stripe is hyped so much yet they still use | ruby... | clintonb wrote: | Is the implication that if Stripe started with another | language, the company would be worth more? What's the problem | with Ruby? | henning wrote: | tootie wrote: | Types for Ruby, types for JavaScript, types for Python. Why | didn't we all just stick with Java? | xtracto wrote: | Young people are coming around ... I used to write code in C, | C++ then Java, C# (.NET v1). I could never understand how | people could implement large systems with dynamically typed | languages such as Ruby or Python. They are great for smallish | scripts, but once your codebase (and team) grows, they become a | nightmare to maintain. | | In my experience, large codebases of those types of languages | have a lot of "magic" thing happen. There's a lot of implicit | stuff that one has to guess or spend time "following the code" | to understand what it is doing. | | And I say this after having built a major lending platform from | scratch in Ruby, including a major Machine Learning scoring | system in Python, having to maintain with a good sized payment | system in pure JavaScript, and nowadays dealing with a major | trading/liquidity system in Ruby. | | They are fun languages, but once the code and systems start to | scale, static typing really helps. For that reason I've seen a | lot of these endeavours try to move to TypeScript or other | typed languages. | klibertp wrote: | I was asked a version of this question by a colleague at work, | namely: "if types are so great, why didn't Python/Ruby/JS | include them from the start (ie. early '90s)?" | | That's because the theory of gradual type systems was only | worked out in the '00s. Before that, you could have a static or | dynamic type system, not anything in between. Common Lisp did | have type annotations, but they were hints for optimization, | without any guarantees. They were also local to subroutines | only. Dylan[3] is an example of an early implementation of the | idea, but Dylan was several years late and, without being able | to compete with Java, died without ever being widely used. | | The proper theory was first established by J. Siek[1] and W. | Taha in 2006. It's distinct from nominal static typing which | uses a single top type (like Object in Java) or generics, and | obviously it's different from both purely static and dynamic | typing. It took almost a decade for the idea to start gaining | practical implementations - I think the original was a made for | Scheme, and one of the first implementations was Typed Scheme | for PLT Scheme, which continues on as Typed Racket[2] today. | Typed Racket is unique in that it enforces the types even on | the untyped side, by wrapping values and exports in contracts. | | The idea proved to be useful in practice, and started being | adopted in various (non-Scheme) dynamically typed languages, | starting with TypeScript for JS and Hack for PHP. On the other | hand, some statically typed languages also became gradually | typed, most notably C#. The implementations continued to | improve, shrinking the parts of their respective languages that | could not be statically typed. In dynamic languages there are | still features that cannot be practically expressed in static | type systems - most metaprogramming and code generation falls | into this category - but they are generally "good enough" for | day to day coding. | | Gradual typing is useful in the same way static type systems | are useful: it can prevent certain kinds of errors by marking | known-invalid expressions without the need to run the code (so, | for example, can help you find errors even in code that's not | covered by tests); it helps in writing tooling for the language | (eg. go to definition, find references); it helps make the code | clearer for the reader (no need to break into a debugger to see | what kind of value a given identifier refers to); in some | implementations it may also help in optimizing the runtime | performance, but that's rare. The "gradual" aspect makes it | easier to adopt when the codebase grows larger - the bigger the | codebase, the more useful static types are, but by the time the | codebase grows large enough to justify static typing it's too | big to rewrite in a different, statically typed language. | | In short: writing small projects or prototypes in a dynamically | typed language is faster while maintenance and expansion of | large projects is easier in statically typed one. Gradual | typing lets you go from one to the other without a huge cost of | a full rewrite. | | [1] https://wphomes.soic.indiana.edu/jsiek/what-is-gradual- | typin... | | [2] https://docs.racket-lang.org/ts-reference/index.html | | [3] https://opendylan.org/index.html | rco8786 wrote: | It's not like Java is the first or only language with types... | ecshafer wrote: | I've worked with Ruby + Sorbet, and also with Java. I would | rather write Ruby + Sorbet than Java right now. Ruby is a | really nice language. | | Though Java still has some great strengths, especially the 8+ | functional programming features and the concurrency library is | great. If I could use Rails with Java it might be a different | story though, since I hate Spring. | jez wrote: | Hey! I wrote this article. If you have any questions about Sorbet | or Stripe, please don't hesitate to ask! | cmer wrote: | I added Sorbet to my codebase right after reading the article, | but it seems to be expecting that I annotate every single one | of my gems. Is this accurate? Is there a way around this? | jez wrote: | Somewhat. They don't all need super specific types for every | method they've defined, but Sorbet does at least need to know | all the classes, modules, and constants in use in your | codebase, whether those come from code you've written or code | inside gems. | | But there's tooling (first-party and third-party) that will | either download or generate RBI files defining constants that | come from gems. `srb init` is the first party solution, and | Shopify's `tapioca` gem is the most popular third-party | solution[1]. | | Unfortunately, because Ruby doesn't have import statements at | the top of every file, Sorbet can't just do something like | silently treat unknown imports as not having a type (like | TypeScript and Flow can do), because then it would never be | able to tell between "exists but unknown" vs "typo; does not | exist" for constant definitions. This definitely makes the | adoption process a little tricker compared to other | languages, but it's generally a one-time thing once you've | got the tooling set up. | | Also if you're ever having trouble getting the tooling to | work, there's a lot of people chatting about Sorbet daily at | https://sorbet.org/slack | | [1] https://github.com/Shopify/tapioca | sankha93 wrote: | Hey, nice work with Sorbet! I am one of the grad students who | worked on RDL, one of the early research projects related to | Ruby type systems. What are the next set of challenges that a | tool like Sorbet needs to solve? I see you mentioned meta- | programming in the blog post, is that something that is handled | well by Sorbet? Sorry if this is already handled, I haven't | been up to date with the latest features of Sorbet. | jez wrote: | Thanks for your work on RDL! The post didn't mention it, but | Sorbet still owes most of its type definitions for the Ruby | standard library to RDL's original annotations. We just | borrowed them and changed the syntax. | | Our general approach to metaprogramming at the moment has | been two-fold: | | - Use ahead-of-time code generation powered either by runtime | reflection or ad-hoc static analysis to generate RBI files | declaring things that have been metaprogrammed. - Build type | system features, errors, and autocorrects that encourage | people to structure their code in ways that doesn't require | metaprogramming to solve. | | Metaprogramming is definitely still a sticking point, but the | existing solutions work ~okay and the rest of the upside | Sorbet provides make it worthwile to power through. | | Next challenges: | | - Make it faster. While the post was talking about how fast | it is, it wasn't telling the whole truth. Turns out some type | checking operations in a 15 million line codebase are still | slow, and we're working on making those faster. | | - Add more IDE features. At the beginning of this year I put | a lot of work into making Sorbet's parser more tolerant of | syntax errors, which helps things like autocompletion work | better. We also want to make more code actions, autocorrects, | and refactoring tools, to bring Ruby in line with what you'd | expect from other typed languages in the IDE experience | | - Add more type system features. Shapes and tuples are a huge | unimplemented feature still, and people ask about it all the | time. There are a handful of other type system features | (happy to list them if you're curious) that would also let | people write idiomatic Ruby and still have good typing. | | Lots left to do! | mwint wrote: | I write a lot of Ruby for work, but I'm not sure what | "shapes" are - do you have any good reference where I could | start reading? | jez wrote: | It's what TypeScript calls object types: | | https://www.typescriptlang.org/docs/handbook/2/objects.ht | ml | | (Ruby and JavaScript mean slightly different things by | the word "object" so we chose a different word.) | | Flow also has a distinction between exact and inexact | object types: | | https://flow.org/en/docs/types/objects/ | | where the difference is whether other, unspecified fields | are allowed to hide in the object, or whether values of | type `{foo: number}` must have _only_ the `foo` field, | and no other fields. | burlesona wrote: | `srb init` has had a lot of problems since Ruby 3.x, and while | I haven't tried in a few months it looks like there's recent | issues that it still doesn't work | (https://github.com/sorbet/sorbet/issues/5332). Is the advice | just to use Tapioca instead of `sub init` at this point? | jez wrote: | Hoping to have this specific issue fixed either today (or by | the end of the week at the latest). So sorry for the delay in | getting around to this! | vhodges wrote: | What is the status of the AOT compiler? Still moving forward? | Stuck for lack of resources? Release planned soon? :). | chucke wrote: | When is RBS support coming? | felipeccastro wrote: | I tried adding this to a new Rails project with no luck. Is | there a sample Rails app with Sorbet fully configured (i.e. | most gems typed) available on github for reference? | jez wrote: | Unfortunately I don't know of an example repo, but I do know | that most projects (except Stripe) who use Sorbet use it with | Rails. There's a #rails channel on Slack--maybe you'd like to | try asking there! | | https://sorbet.org/slack | lasvad wrote: | With Ruby 3 releasing with RBS, for new projects, whats the | current advised path? Native RBS or Sorbet? Can they co-exist | and if so, is there a point to using both? | | Sorbet is something I've been interested in using for a couple | years and finally got a round to actually trying it out. I | tried to use Sorbet with ruby 3.1.1 but unfortunately it didn't | "just work" which I think is crucial for mass adoption. I want | to give the benefit of the doubt and say its my local env that | causing issues with Sorbet but in a fresh `rails new test_app | --api` project, I'd expect `srb init` to work without errors... | maybe I need to give it another go, curious on your thoughts | above tho! :) | jez wrote: | We discovered a bug in `srb init` for Ruby 3.1 recently that | a teammate of mine is working on fixing at the moment. It's | likely that if you tried again in a few days it'll have been | fixed. Sorry about that, totally agree that the out-of-box | experience should just work. | | I wrote up an FAQ about the state of Ruby 3 and RBS here: | | https://sorbet.org/docs/faq#when-ruby-3-gets-types-what- | will... | | The tl;dr is that RBI files (not RBS files) will probably | always be the preferred way to declare types for third party | code (because it will always support exactly the same set of | features that Sorbet does). We have some people in the | community look into teaching Sorbet to read the RBS format, | but the existing parsers for RBS files are written in Ruby | and are very slow, and there are some ambiguities in the spec | that make writing a third party parser that compiles to | native code tricky. You can see an attempt to write a fast | RBS parser in C++ here[1], but again given that RBI files do | everything we need them to right now and we have other | features people are asking us for, we haven't prioritized RBS | support incredibly highly. | | Sorbet works completely fine without RBS files! | | [1] https://github.com/Shopify/rbs_parser | Fire-Dragon-DoL wrote: | I tried using sorbet on our project, but the type system it | supports is way too poor. The most glaring problem is the lack of | support for duck typing, only nominal typing is supported, no | structural typing. | | For key parts of the code, there was no type safety where we | expected it. | | In the end, it felt far from being like Typescript, we opted for | removing it, instead we added some runtime type checking and we | document with YARD. Far from ideal, but that's the tooling | available. | | The gem integration is terrible currently: we wrote the gem, | fully typed with sorbet, but for some reason the type checking | was completely ignored in the main project where we referred it | bbrree66 wrote: ___________________________________________________________________ (page generated 2022-03-29 23:00 UTC)