Ruby 3.0: Actor model and GenServer implementation

I was looking at some news about Ruby 3 (see Ruby 3.0.0 Released) and came also across this GenServer implementation which I thought I’d share here:

What do you folks think about Ruby 3? What kind of applications does it open in Ruby world that were previously not feasible? How does it compare to Elixir, what might be the implications, good or bad.

Just to be clear, I am curious about your opinions, not in some pointless Ruby vs Elixir flamewar. I love both Elixir and Ruby and I am pretty sure many people in this forum feel the same.

4 Likes

I haven’t read Ruby 3 feature list yet, but IMO, GenServer gets its power from OTP/BEAM and its ability to run long running processes. You can store arbitrary data in the process and use it however you want. I am not sure actor model is powerful without a VM. What do others think?

3 Likes

Some quick observations:

  • mixing mutable objects with an idiom intended for immutable data feels weird; an instance of Stack is threaded through every call, but functions like Stack#push mutate the data inside it, leading to code with hidden connections:
    case msg
    in [:push, value]
      push(value) # <= mutates self
      [:noreply, self]
    end
  • corollary: If a handler accidentally returns a reference to state to a caller, now you’ve got unsynchronized access to a shared mutable data structure :skull_and_crossbones:

  • the handle_cast / handle_call idiom is very dependent on pattern-matching in function heads; while it’s not semantically different to have a single body with a giant case, it doesn’t seem as readable to me.

  • overall, I’m not sure if trying to exactly reproduce the gen_server plumbing is the right approach. I’ll be very interested to see what people build with more Rubyish idioms.

2 Likes

As others pointed out, the runtime is much more important than the actor model (the model is not some magic by itself). After all, we have actor systems implemented in many languages and I’ve heard people walk away from Akka and Orleans disappointed due to lack of part of the guarantees that the BEAM VM has (although let’s be objective here, most users of these libraries are satisfied).

If people use some sort of a linter / static code checker in combination with Ruby’s actor support that ensures only immutable data are passed around (I believe you can achieve this in Ruby with the .freeze method), then it could work. But then you also need a reliable messaging inbox implementation for all actors.

Me too.

1 Like

Wondering if the start_link even makes sense in Ruby? Does it really link anything?

3 Likes

I think it’s a great feature that has the potential to bring a solid concurrency pattern to Ruby. I think that it is quite amazing that Ruby adopted the actor model: this has the potential to make Ruby even better than it already was.

Is this making Ruby preferable to Elixir/Erlang for concurrency? Most likely not, because of the issues with mutability mentioned above, and the lack of a VM designed from the beginning for safe concurrency. But I believe that’s not the point: this feature makes Ruby better, and applicable to more problems than it used to be.

I think about this as beneficial “cross-pollination” between languages, not as a competition.

As an engineer that loves both Ruby and Elixir, this feature makes me happy, as it expands the creative space of my favorite tools. If I am designing specifically for concurrency, I will still reach for Elixir. But in a project that fits Ruby better, this makes it possible to introduce concurrency where needed, using a pattern I like.

As for the GenServer implementation, I look at it more as an example to understand how Ractors work, than as a piece of code I would introduce in a real app. It wouldn’t make real sense to port exactly OTP GenServers to Ractors, as the underlying system is different. In Ruby, I would use the actor model idiomatically, without trying to mimic OTP too closely.

6 Likes

It seems they also have ractor supervisors, so it might make sense.

I have seen several Rust libs (frameworks?) that employ the actor model and they all go subtly different about it compared to the OTP, which is good and we might need the innovation. It’s nice that not everyone is trying to yet again prove the impossible thesis that shared state with naked OS threads will work for sure this time. :003:

Yeah, I’d say gen_server interface is the worst. Ok. I’m exaggerating (I also appreciate that there is a reason why gen server looks like it does), but I really take Dave thomas famous “dogs breakfast” criticism to heart. Only reason I don’t use a package to organize it is because I think it’s bad form to add an unnecessary abstraction layer into the mental model for a commonly accepted pattern… One of these days I’ll do “isaacs best practices with gen_server” video, but I’m baffled by why someone working with ruby would ever want to repeat what exists verbatim, except that maybe they are worried about losing relevance to elixir.

2 Likes

It’s funny how things come together. I don’t know the Ruby 3 feature list, but I learned of an actor framework in Ruby that was strongly influenced by OTP (GitHub - celluloid/celluloid: Being Revived: Actor-based concurrent object framework for Ruby) about the same time I heard of Elixir in 2013, and even then, I was more interested in Elixir because it was running on BEAM and could take advantage of its guarantees.

2 Likes