Honest question from someone working on a non-negligible Rails codebase: what would be my gains, were I to switch to Elixir?
I've watched Elixir with much interest from afar, I even recently cracked open the book on it, but I feel like my biggest pain points with Ruby are performance and lack of gradual typing (and consequent lack of static analysis, painful refactoring, etc), and it doesn't really seem like Elixir has much to offer on those. What does Elixir solve, that Ruby struggles on?
Performance is a complex story. Elixir is very good at massive amounts of parallel computing, and uses this to handle each request (or socket) in it's own contained manner, which nets you some simplicity in designing systems and scaling. However it's not very good at single threaded off-the-block performance (but neither is Ruby)
Typing is coming, some is already here, and if you're impatient you can use dialyzer to get half way there. But in my experience you need it less than you'd think. Elixir primitives are rather expressive, and due to the functional nature there really isn't any "new" data structure that isn't easy to dive into. And with each function head being easy to pattern match in, you can dictate the shape of data flow through your application much as you'd do in a typed language.
The ide story isn't great, but it's getting better. There are a few LSPs out there, and an official one coming soon. And I'd say all of them beat solargraph handily
But most of all I'd say that, since it's a bit more strict, elixir saves you and your coworkers from yourselves and each other. There are still several ways to do something, like Ruby, but going out of your way to write very cutesy code that the next programmer will loathe is more difficult. Not impossible, but harder. And with the rock stable concurrency systems in place, a lot of the impetus to come up with clever solutions isn't there
Re types, I think what I really want is just comprehensive static analysis. Coming from Rust, where I can immediately pull up every single call site for a given function (or use of a struct, etc.), I find refactoring Ruby/Rails code to be comparatively very painful. It is several orders of magnitude more laborious and time-consuming than it should be, and I just don't find the justification for that cost convincing - I'd trade every last bit of Rails' constant runtime metaprogramming cleverness for more static analysis.
What I like about Rails is its batteries-included nature, but I honestly could do without those batteries materialising via auto-magic includes and intercepted method calls. I appreciate that that's just the culture of Ruby though, so I don't expect Rails to ever change.
The lack of cutesiness in Elixir sounds lovely. I don't know if functional approaches could make up for a lack of typing for me; I think I'd need to try it. I've used and enjoyed Haskell, but of course that's very strongly typed.
Elixir has had some static analysis for a long time, you can use a command to see all call sites of a module and function. It's useful, and most of the LSPs use (or used to use) it. The newer versions also are adding various hooks to the compiler, to allow for better tooling
As for magic, elixir can have what looks like magic, but upon closer look it's nothing more than macros, which are generally pretty easy to decipher and follow. It has minimal automatic code generation, what it has is mostly the form of generators you'd run once to add things like migrations
I have the same experience with typing in Elixir. It's hard to explain without experiencing it yourself, but the dynamic typing just doesn't feel like as big of a deal as it might in other languages. Elixir's guardrails (such as pattern matching in function heads, which you mentioned) get you most of the benefits - and you still get the convenience and simplicity of a dynamic language. It's a great balance.
I'm looking forward to the upcoming gradual type system - it can only be an improvement - but I would still encourage people to try Elixir now, and let go of their preconceptions about static typing.
I cracked open an Elixir book last night, and with the benefit of a few chapters, I can see how Elixir's pattern matching can obviate some of the issues I have with purely dynamic, Rails style programming.
I also note that there appears to already be more static typing going on than I realised. In your add_comment/2 code, for instance, you focus on the {published: true} pattern matching. That is very neat, but what stands out more to me is that all clauses of that function require BlogPost, a struct type.
Am I right in thinking that every instance of BlogPost type must be constructed explicitly in your code? I.e. that every possible instance of BlogPost in the code base is knowable at compile time, along with its entire life cycle?
Or does Elixir partake of the horror that is duck typing, where any conforming untyped map of indeterminate provenance will pass the guard check for a BlogPost?
> Would you like another undefined method exception on that NilClass?
Don't take my word for it but IIRC structs are implemented as maps with a __struct__ key with the struct name, and that's used to implement checks and balances at different levels of compilation, linting and so on.
In practice I find that I hardly ever need to think about things like this. A few times I've done macro expansion to peek under the hood and figure something out but that's partially Lisp damage, I could probably just as well have read some documentation.
> Am I right in thinking that every instance of BlogPost type must be constructed explicitly in your code? I.e. that every possible instance of BlogPost in the code base is knowable at compile time, along with its entire life cycle?
Almost. You can create a struct dynamically at runtime like this:
struct(BlogPost, %{title: "The Title"})
# => %BlogPost{title: "The Title"}
… but you rarely need to. In fact I'm not sure I've ever used `struct/2` in real life. 99.9% of all the structs you ever create will have their types known at compile-time.
> any conforming untyped map of indeterminate provenance will pass the guard check for a BlogPost?
Nope. In the `add_comment/2` example, If I pass anything except a %BlogPost{} to that function, I'll get an error.
While I remain haunted by thoughts of someone e.g. deserialising a YAML file into a map, which then sneaks in some __struct__ key and squeaks past the guard clause, I also appreciate this seems fairly unlikely in practice. I think I'm just traumatised by Rails. It sounds like the culture around Elixir eschews excessive cutesiness, though. Promising!
> Performance of what, exactly? Hard to beat the concurrency model and performance under load of elixir.
The performance of my crummy web apps. My understanding is that even something like ASP.NET or Spring is significantly more performant than either Rails or Phoenix, but I'd be very happy to be corrected if this isn't the case.
I appreciate the BEAM and its actor model are well adapted to be resilient under load, which is awesome. But if that load is substantially greater than it would be with an alternative stack, that seems like it mitigates the concurrency advantage. I genuinely don't know, though, which is why I'm asking.
Some of the big performance wins don’t come from the raw compute speed of Erlang/Elixir.
Phoenix has significantly faster templates than Rails by compiling templates and leveraging Erlang's IO Lists. So you will basically never think about caching a template in Phoenix.
Most of the Phoenix “magic” is just code/configuration in your app and gets resolved at compile time, unlike Rails with layers and layers of objects to resolve at every call.
Generally Phoenix requires way less RAM than Rails and can serve like orders of magnitude more users on the same hardware compared to rails.
The core Elixir and Phoenix libraries are polished and quite good, but the ecosystem overall is pretty far behind Rails in terms of maturity. It’s manageable but you’ll end up doing more things yourself. For things like API wrappers that can actually be an advantage but others it’s just annoying.
ASP.NET and Springboot seem to only have theoretical performance, I’m not sure I’ve ever seen it in practice. Rust and Go are better contenders IMO.
My general experience is Phoenix is way faster than Rails and most similar backends and has good to great developer experience. (But not quite excellent yet)
Go might be another option worth considering if you’re open to Java and C#
Thank you, I really, really appreciate the thoughtful answer.
I've written APIs in Rust, they were performant but the dev experience is needlessly painful, even after years of experience using the language. I'm now using Rails for a major user-facing project, and while the dev experience is all sunshine and flowers, I can't shake the feeling that every line I write is instant tech debt. Refactoring the simplest Rails-favoured Ruby code is a thousand times more painful than refactoring even the most sophisticated system in Rust. I yearn for some kind of sensible mid-point.
Elixir seems extremely neat, but I've been blocked from seriously exploring it by (a) a sense that it may not be more any more performant than Ruby, so why give up the convenience of the latter, and (b) not having seen any obvious improvement on Ruby's hostility to gradual typing / overuse of runtime metaprogramming, which is by far my biggest pain point. I'm chuffed to hear that the performance is indeed better, that the magic in Phoenix happens at compile time, and that gradual types are being taken seriously by the language leadership.
There's three reasons to choose elixir or perhaps any technology
The community and it's values, because you enjoy it, because the technology fits your use case. Most web apps fit. 1 and 2 are personal and I'd take a 25% pay cut to not spend my days in ASP or Spring, no offense to those who enjoy it.
I've watched Elixir with much interest from afar, I even recently cracked open the book on it, but I feel like my biggest pain points with Ruby are performance and lack of gradual typing (and consequent lack of static analysis, painful refactoring, etc), and it doesn't really seem like Elixir has much to offer on those. What does Elixir solve, that Ruby struggles on?