What's particularly exciting is how this positions Ruby for modern workloads. With proper parallelism, Ruby apps can finally compete with Go and Node.js in concurrent scenarios without sacrificing developer happiness.
The typing improvements also can't be understated. Gradual typing strikes the right balance - it helps teams scale codebases without forcing the verbosity of Java or the complexity of TypeScript's type gymnastics.
Looking forward to seeing how the Rails ecosystem adopts these features. This could spark a Ruby renaissance in 2025.
Is it against the rules to say a comment is clearly AI generated?
The ruby::box thing looks pretty interesting, from a cursory glance you can run two simultaneous versions of something like a feature or rollout much more conveniently.
Also being able to do
if condition1
&& condition2
...
end
on multiple lines rather than one - this is pretty nifty too!I'm not actually in need of this feature at the moment, but it would be cool and I think it fits very well with the idea of ractors as being completely separated from each other. The downside is of course that sharing objects between ractors would get slower as you'd need to copy the objects instead of just sharing the pointer, but I bet that for most applications that would be negligible. We could even make it so that on ractor creation you have to pass in a box for it to live in, with the default being either a new box or the box of the parent ractor.
Ruby::Box wouldn't help reducing contention further, they actually make it worse because with Ruby::Box classes and modules and an extra indirection to go though.
The one remaining contention point is indeed garbage collection. There is a plan for Ractor local GC, but it wasn''t sufficiently ready for Ruby 4.0.
Assuming you mean "because with Ruby::Box classes and modules have an extra indirection to go though." in the second paragraph, I don't understand why that would be necessary. Can't you just have completely separate boxes with their own copies of all classes etc, or does that use too much memory? (Maybe some COW scheme might work, doodling project for the holidays acquired haha)
Anyway, very cool work and I hope it keeps improving! Thanks for 4.0 byroot!
Yes, Ractor local GC is the one feature that didn't make it into 4.0.
> Can't you just have completely separate boxes with their own copies of all classes etc, or does that use too much memory?
Ruby::Box is kinda complicated, and still need a lot of work, so it's unclear how the final implementation will be. Right now there is no CoW or any type of sharing for most classes, except for core classes.
Core classes are the same object (pointer) across all boxes, however they have a constant and method table for each box.
But overall what I meant to say is that Box wouldn't make GC any easier for Ractors.
if condition1 &&
condition2
...
end
for ages and it seems to work find, what am I missing with this new syntax?!I spent over a decade enjoying Ruby and even wrote a book about it. At this point, though, Python has won for me: fastapi, pytorch, langchain, streamlit, and so on and on.
It's a bit sad, but I'll always remember the Christmas gifts, and the syntax that is always so much better than Python.
I just can’t stand the excessive dynamism of Ruby. I understand some people prefer/enjoy it, it’s just not for me.
For me, the killer feature of Python was the typing module and the intellij pycharm community edition being free and RubyMine having a subscription fee.
Luckily people seem to be aware of this and there was a whole talk about improving Ruby DX.
I vaguely remember reading Shopify is using Fiber / Rack / Async in their codebase. I am wondering if Rails will get more Fiber usage by default.
The Ractor experimental status could almost be removed. They no longer have known bugs, and only one noticeable performance issue left (missing Ractor local GC).
But the API was just recently changed, so I think it's better to wait another years.
> I vaguely remember reading Shopify is using Fiber / Rack / Async in their codebase.
Barely. There is indeed this management obsession for fibers even when it doesn't make sense, so there is some token usage there and there, but that's it.
There is one application that was converted from Unicorn to Falcon, but falcon isn't even configured to accept concurrent requests, the gain is basically 0.
As for Rails, there isn't much use cases for fibers there, except perhaps Active Record async queries, but since most users use Postgres and PG connections are extremely costly, few people are using AR async queries with enough concurrency for fibers to make a very noticeable difference.
They indeed replaced Unicorn by Falcon in one application, but falcon is configured in "Unicorn mode" (no concurrent requests). So the gain is effectively 0.
Also note how they don't share any performance metrics, contrary to https://railsatscale.com/2023-10-23-pitchfork-impact-on-shop...
It's a bit of a mess IMO. I'd much prefer everything be simplified aggressively in regards to threads + GIL; and Ractors integrated on top of Ruby::Box to provide not only namespaced container-like entities but also thread-support as a first-class citizen at all times. The API of ractors is weird and really not fun to use.
I wanted to read the source package files directly because I always found `shared-mime-info`'s usual two-step process for adding or editing any of the XML type data to be annoyingly difficult and fragile. One must run `update-mime-database` to decompose arbitrarily-many XML packages into a set of secondary files, one all-file-extensions, one all-magic-sequences, one all-aliases, etc. System package managers usually script that step when installing software that come with their own type data. I've accidentally nuked my entire MATE session with `update-mime-database` before when I wanted to pick up a manual addition and regenerated the secondary files while accidentally excluding the system path that had most of the data.
I ended up doing it with four Ractors:
- a Ractor matching inputs (MIME Type strings, file extensions, String or Pathname or URL paths for sniffing) against its loaded fully-formed type definition objects.
- a Ractor for parsing MIME Type strings (e.g. "application/xml") into Hash-keying Structs, a task for which the raw String is unsuitable since it may be overloaded with extra syntax like "+encoding_name" or fragment ";key=value" pairs.
- a fast XML-parser Ractor that takes in the key Structs (multiple at once to minimize necessary number of passes) and figures out whether or not any of those types are defined at all, and if so in which XML packages.
- a slow XML-parser Ractor that takes the same set of multiple key Structs and loads their full definition into a complete type object, then passes the loaded objects back to the matcher Ractor.
The cool part of doing it this way is that it frees up the matcher Ractor to continue servicing other callers off its already-loaded data when it gets a request for a novel type and needs to have its loader Ractors do their comparatively-slow work. The matcher sets the unmatched inputs aside until the loaders get back to it with either a loaded type object or `nil` for each key Struct, and it remembers `nil`s for a while to avoid having to re-run the loading process for inputs that would be a waste of time.
The last pre-Ractorized version allocated around 200k objects in 7MiB memory and retained 17k objects in 2MiB of memory for a benchmark run on a single input, with a complete data load. The Ractorized version was twice as fast in the same synthetic benchmark and allocated 20k objects in 2MiB of memory and retained 2.5k objects in 260KiB of memory for its initial minimal data load. I have it explicitly load `application/xml` and `application/zip` since those combined are the parent types for like a third of all the other types, and a few other very common types of my choosing.
I think a lot of the barrier to entry for Ractors isn't the API for the Ractors themselves but in figuring out how to interact with Ractorized code from code that hasn't been explicitly Ractorized (i.e. is running in the invisible “main” Ractor). To that end I found it easiest to emulate my traditional library API by providing synchronous entry-point methods that make it feel no different to use than any other library despite all the stuff that goes on behind the scenes. The entry methods compose a message to the matcher Ractor then block waiting for a result or a timeout.
I also use Ractors in a more lightweight way in my UUID/GUID library where there's a Ractor serving the incrementing sequence value that serves as a disambiguator for time-based UUIDs in case multiple other Ractors (including invisible “main”) generate two UUIDs with the same timestamp. Speaking of which, I'm going to have to work on this one for Ruby 4.0, because it uses the removed `Ractor.take` method.
I am installing it now. Thank you Matz and team.
I like it, it deserves attention, especially for those who are seeking for typed Ruby. With this, you can finally experience it, and the syntax feels more ergonomic than with Sorbet.
Out of all three I think Shopify have the highest possibilities. There may be additional usefulness interms of ZJIT.
Adding static typing to a dynamic language mostly gives you the disadvantages of both, without a lot of benefits. It's better to stick to languages that were designed with static types from the start.
I love programming in Ruby, having to worry about type annotations and the additional constraints that come with them would take a lot of the fun out of that.
Speed isn’t why people choose Python.
Python is one of the Lingua Franca of scientific, data, and most importantly, ai communities
Has much bigger community than Ruby
Has much, much better tooling story
Has much better gradual typing story
Isn’t THAT much slower than Ruby, there are far more attractive targets than Ruby if you care about performance
There's a pretty battle tested tool to define inline types as ruby syntax and type check both statically and at runtime[0].
It's still not a particularly nice situation imvho compared to typescript or python, but there's been some movement, and there's a newsletter that follows static typing developments [1] which may give you some insights.
Really rough around the edges, lots of stubs have to be added because support for gems is lackluster but whatever Sorbet generates are hit or miss etc. So you end up writing a lot of hard to understand annotations and/or people get frustrated and try to skip them etc.
Overall a very bad DX, compared to even typed Python. Don’t even want to compare it to TS because then it becomes really unfair.
IMHO if we wanted to write types in our programming language we would not have chosen Ruby for our programming tasks. We would have chosen one of the zillion of other languages. There were a lot of them when Ruby got traction about 20 years ago and many other languages have been created after then. It's not surprising that one of the main proponent of typing in Ruby is Shopify, because their path away from Ruby is very costly.
In my case one of the reasons I invested in Ruby is precisely because I did not have to write types.
Does it make Ruby slower than Java, my main language in 2005? Yes.
Is it fast enough for my customers? Yes. Most of them decided to use Ruby, then hired me.
Do I have to write unit tests to check for types? I don't.
Occasional problems that static types would have prevented to happen? Once or twice per year. Overall that's a good tradeoff because pleasing the type checker for non trivial types can be a time consuming task and some errors happen at runtime anyway, when the real world hits with its data a carefully type checked code base or a carelessly dynamic typed one. Think of an API suddenly returning a bad JSON, maybe an HTML 500 page. Static or dynamic typing, both won't help with that.
For generating (with LLMs) API clients and CLIs it’s especially useful—define the shape once, get validation at ingress/egress for free.
Maybe momentum is happening in new projects rather than retrofits? [0] https://oss.vicente.services/dspy.rb
It’s not like Ruby becomes Haskell. But it does provide a good deal of additional saftey, less testing, LSP integration is good, and it is gradual.
There is a performance hit but we found it to be quite small and not an issue.
But there are area of our application that use Grape and it is too meta for Sorbet so we don’t try and use it there.
I’ve been using this pattern for API clients[0] and CLIs[1]: define the shape once with Sorbet, get automatic JSON Schema generation when you need it.
[0] https://github.com/vicentereig/exa-ruby [1] https://github.com/vicentereig/lf-cli
I think most people who cared just moved to typescript.