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! :)
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!
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!
https://www.typescriptlang.org/docs/handbook/2/objects.html
(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.
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..)?
The mechanism in the post is the same mechanism in use today with Sorbet signatures.
To get the `sig` method in scope, you have to put `extend T::Sig` in that class (or one of its parents). When `sig` is called for the first time in a class, it monkey patches that class to install some overrides of the method_added method. Ruby calls this method_added method for you every time it creates a method (from any means, static or dynamic). Code is here:
https://github.com/sorbet/sorbet/blob/master/gems/sorbet-run...
> How is the story for sorbet and vim/nvim?
Great, honestly better than VS Code for everything except autocompletion. I personally use Sorbet with Neovim.
> 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..)?
Not that I know of unfortunately, but I also pay more attention to the type checker and it’s bugs than the tooling people build around it to get real world work done
# 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.You're right that Sorbet does not yet catch these bugs yet, and we'll likely get around to it in the future. It's being tracked in this issue[1].
We haven't quite prioritized this yet partly because it doesn't actually prevent you from using Sorbet in a Ruby 3 codebase, it just won't report all the errors it could (e.g., Sorbet allows using `T.untyped`, which can also cause runtime errors to go unreported).
Now that you've jogged my memory, there used to be some weirdness in our internal representation for method calls that made implementing this feature tricky. But we've since refactored some internal data structures and now it's probably a lot easier. Maybe I should look into how much work this would be again ...
Is that supposed to be how it works?
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