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
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.
.Net CodeContracts were this for me, but that was never fast. C# is a statically-typed language, so what kind of types could you further add to it? As a trivial example, maybe you want to ensure that `invoice_id` consists of at least 20 characters. CodeContracts would then statically attempt to prove that assertion for you. They had meticulously annotated the entirety of the .Net stdlib, and those annotations were spot-on.
This obviously helps with things like `null`.
It completely changed the way I code. You have to think a little bit more about how you structure your code if you plan to hand it off to a theorem prover. I unlearned several bad habits (I unlearned even more with Rust).
> 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.
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!
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.
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! :)
`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?
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?
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.
Thank you for writing up such a nice overview. I've been busy trying to get tests into various legacy projects I've inherited - definitely interested in tackling types next.
Couple of questions:
> The declare_method call above acted like a decorator on the def call method: it would check that the msg argument given to call was a String and that call returned a String on every invocation
How was this implemented? Dynamically altering Object#send or something along those lines?
How is the story for sorbet and vim/nvim?
Are there "run time" or "code gen" uses for sorbet? Like generating swagger/openapi documentation/schemas based on typed Api methods? Or vice-versa - scaffolding sorbet-typed Api from a swagger.json? Or something similar for graphql (or, well, SOAP..)?
Overall sorbet has been good and useful, so thank you. But you just have to be careful when you're not using the same version of ruby that stripe uses. I recently got hit by a bug:
# typed: strict
extend T::Sig
sig {params(x: Integer, y: String).void}
def run(x:, y:)
puts(x + 1, y)
end
args = {
x: 1,
y: "Hi"
}
run args
type checks just fine in sorbet, but is an error in ruby 3+. It works though (with errors) in 2.7 and lower which is what Stripe uses from what I gather.
I tried using sorbet through it's LSP in neovim. I would get diagnostics not after every change, but after every change that adds/removes a function definition. Effectively, I would have to delete and add back a function definition, or the "end" keyword, to trigger sorbet to type check my code.
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?
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.
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.
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!
Sorbet and/or RBS seems like they may be the future given how popular typescript is with JS programmers these days. There are some other projects that assist programmers without relying on formal type definitions in the source or shadow typing files:
Solargraph combines inference and insight from YARD docs (standard for many gems, plus Castwide has written more YARD for the standard library) to make some pretty good guesses. Crucially it has plugins that add the insights from popular gems with static analysis (e.g. reek, rubocop). I maintain solargraph-rails, which parses your Ruby to make guesses about (surprise) Rails.
The typeprof gem can help IDE plugins make typing guesses based on your tests. This project is interesting to me because it's going into Ruby 3.1 so I think it reflects awareness from the core ruby team that many programmers are not ready to add types to their code.
Sorbet is very useful, but the ergonomics suck. It’s fucking difficult to write rspec tests. The performance overhead of writing sorbet on rails in a big codebase is so much that we have turned it off. The pre-interpreter type checking is somewhat useful.
The alpha releases are also a big concern. We are stuck on a 300 commit (release) old build and can never upgrade safely.
We have also never been able to get the VSCode extensions to run.
Thanks for Sorbet, but I’d suggest people outside Stripe to look elsewhere.
My problem with Sorbet is how ugly it looks. It's probably not possible to do with Ruby (now) but Crystal does it better[1] while (mostly) retaining the syntax of Ruby. I suppose it's trying for Elixir's @spec, which I like less than Crystal's sigs but still prefer over this.
To be honest I struggle with the frustration of programmers of dynamic languages building their own (mostly half-assed) type checkers, after years and years of dealing with their bogus arguments about why dynamic languages are superior and all you need is unit tests (the same cohort is also now on a misguided crusade against unit tests but that's a whole other story).
At least the JS crowd had the decency to buy into a whole new (far better) language instead of a bolt-on solution.
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.
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.
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.
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
Sorbet is sooo close to being idiomatic in Ruby. With some support for duck typing and roughing out edges around memoization / ivars, I could see it being adopted into Ruby proper.
I spent about two weeks trying to use Sorbet while working on a rails codebase. For context, I'm a huge typing fan, and use it even when writing 10 line scripts in Python. Here were my feelings:
The TLDR for me is: I’d still be willing to keep using sorbet, if issue number 4 below (that the LSP isn’t very responsive) would be fixed. Otherwise, it adds more work than it removes from my workflow, so I've stopped trying it out.
Start with the negatives:
1. It’s a lot of grunt work to set it up properly in an application like ours with many dependencies. Specifically, sorbet and/or related tooling tries to generate RBI (equivalent of typescript’s index.d.ts) files by actually importing and running your code, and doing introspection on the types of the arguments of functions. I managed to work around this problem, but it’s a reflection of how young Ruby typing is as a whole that this step was a time-sink for me.
2. There’s no mechanism to do the equivalent of yarn add -D @types/react-table (which would install typings for the react-table package). You basically have to copy paste from this github repo[0] manually.
3. Some really popular gems still don’t have types. For example, IIRC, devise’s typings are either non-existent or are uselessly incomplete.
4. The sorbet LSP isn’t very responsive, at least in neovim. I asked @jez about this just now, so hopefully I'll get a response.
5. Super verbose syntax.
Positives:
1. Thanks to this repo[1], there’s actually a way to easily generate typings that would cover a lot of the dynamism of rails, it works quite well.
2. It’s legit helped me catch errors with my code.
[+] [-] reichertjalex|4 years ago|reply
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!
[+] [-] itslennysfault|4 years ago|reply
[+] [-] zamalek|4 years ago|reply
This obviously helps with things like `null`.
It completely changed the way I code. You have to think a little bit more about how you structure your code if you plan to hand it off to a theorem prover. I unlearned several bad habits (I unlearned even more with Rust).
[+] [-] brandonbloom|4 years ago|reply
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.
[+] [-] clintonb|4 years ago|reply
[+] [-] hardwaresofton|4 years ago|reply
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.
[+] [-] alexandre_m|4 years ago|reply
Have you considered using pre-commit?
[+] [-] jez|4 years ago|reply
[+] [-] lasvad|4 years ago|reply
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! :)
[+] [-] burlesona|4 years ago|reply
[+] [-] felipeccastro|4 years ago|reply
[+] [-] sankha93|4 years ago|reply
[+] [-] e12e|4 years ago|reply
Couple of questions:
> The declare_method call above acted like a decorator on the def call method: it would check that the msg argument given to call was a String and that call returned a String on every invocation
How was this implemented? Dynamically altering Object#send or something along those lines?
How is the story for sorbet and vim/nvim?
Are there "run time" or "code gen" uses for sorbet? Like generating swagger/openapi documentation/schemas based on typed Api methods? Or vice-versa - scaffolding sorbet-typed Api from a swagger.json? Or something similar for graphql (or, well, SOAP..)?
[+] [-] willlll|4 years ago|reply
[+] [-] davidatbu|4 years ago|reply
Is that supposed to be how it works?
[+] [-] cmer|4 years ago|reply
[+] [-] vhodges|4 years ago|reply
[+] [-] chucke|4 years ago|reply
[+] [-] hiphipjorge|4 years ago|reply
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|4 years ago|reply
[+] [-] weaksauce|4 years ago|reply
[+] [-] zingar|4 years ago|reply
Solargraph combines inference and insight from YARD docs (standard for many gems, plus Castwide has written more YARD for the standard library) to make some pretty good guesses. Crucially it has plugins that add the insights from popular gems with static analysis (e.g. reek, rubocop). I maintain solargraph-rails, which parses your Ruby to make guesses about (surprise) Rails.
The typeprof gem can help IDE plugins make typing guesses based on your tests. This project is interesting to me because it's going into Ruby 3.1 so I think it reflects awareness from the core ruby team that many programmers are not ready to add types to their code.
solargraph: https://github.com/castwide/solargraph solargraph-rails: https://github.com/iftheshoefritz/solargraph-rails typeprof: https://www.youtube.com/watch?v=UTMj51j9yEg
[+] [-] throwawaypls|4 years ago|reply
The alpha releases are also a big concern. We are stuck on a 300 commit (release) old build and can never upgrade safely.
We have also never been able to get the VSCode extensions to run.
Thanks for Sorbet, but I’d suggest people outside Stripe to look elsewhere.
[+] [-] brigandish|4 years ago|reply
Still, if it helps it helps.
[1] https://crystal-lang.org/reference/1.3/tutorials/basics/60_m...
Edit: Perhaps I spoke too soon https://blog.appsignal.com/2021/01/27/rbs-the-new-ruby-3-typ...
[+] [-] faitswulff|4 years ago|reply
[+] [-] mdoms|4 years ago|reply
At least the JS crowd had the decency to buy into a whole new (far better) language instead of a bolt-on solution.
[+] [-] tootie|4 years ago|reply
[+] [-] xtracto|4 years ago|reply
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.
[+] [-] ecshafer|4 years ago|reply
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.
[+] [-] klibertp|4 years ago|reply
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
[+] [-] paxys|4 years ago|reply
[+] [-] neilwilson|4 years ago|reply
Types are useful when they don't get in the way. As are method contracts.
[+] [-] sethrin|4 years ago|reply
[+] [-] rco8786|4 years ago|reply
[+] [-] Fire-Dragon-DoL|4 years ago|reply
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
[+] [-] gouda-gouda|4 years ago|reply
[+] [-] davidatbu|4 years ago|reply
The TLDR for me is: I’d still be willing to keep using sorbet, if issue number 4 below (that the LSP isn’t very responsive) would be fixed. Otherwise, it adds more work than it removes from my workflow, so I've stopped trying it out.
Start with the negatives: 1. It’s a lot of grunt work to set it up properly in an application like ours with many dependencies. Specifically, sorbet and/or related tooling tries to generate RBI (equivalent of typescript’s index.d.ts) files by actually importing and running your code, and doing introspection on the types of the arguments of functions. I managed to work around this problem, but it’s a reflection of how young Ruby typing is as a whole that this step was a time-sink for me.
2. There’s no mechanism to do the equivalent of yarn add -D @types/react-table (which would install typings for the react-table package). You basically have to copy paste from this github repo[0] manually.
3. Some really popular gems still don’t have types. For example, IIRC, devise’s typings are either non-existent or are uselessly incomplete.
4. The sorbet LSP isn’t very responsive, at least in neovim. I asked @jez about this just now, so hopefully I'll get a response.
5. Super verbose syntax.
Positives: 1. Thanks to this repo[1], there’s actually a way to easily generate typings that would cover a lot of the dynamism of rails, it works quite well.
2. It’s legit helped me catch errors with my code.
[0] https://github.com/sorbet/sorbet-typed
[1] https://github.com/chanzuckerberg/sorbet-rails
[+] [-] throwaway923032|4 years ago|reply
[deleted]
[+] [-] bbrree66|4 years ago|reply
[deleted]
[+] [-] henning|4 years ago|reply
[deleted]
[+] [-] iostream24|4 years ago|reply
[+] [-] ffggvv|4 years ago|reply
[+] [-] clintonb|4 years ago|reply
[+] [-] rco8786|4 years ago|reply