Ruby vs Elixir – Panel discussion at Wroc_love.rb [video]
youtube.com
youtube.com
- Elixir runs on a battle-tested virtual machine, and stuff like supervisor behavior (e.g. auto-restart of crashing processes) is built right in
- Elixir is a functional language, which makes the code you write more testable
- State in Elixir is immutable, which greatly reduces the side effects of running code
I used to develop in Ruby circa 2014-2015. It will always have a special place in my heart, but to be frank, I don't really miss it. Elixir, as a language, is better in almost every way. And once the ecosystem becomes a bit more mature, it will be unstoppable (OK maybe not really, but it's definitely here to stay).
- pattern matching
- immutable data
- built-in supervision tree
- async message passing w/ mailboxes
- networking modules
- much of Elixir is written in Elixir
Here's stuff I'd like to see in the standard library (which may come over time anyway):
- Better time/calendar options (I still like using Timex)
- Rational numbers
- Ruby's BigDecimal in Elixir
- Dialyzer for Elixir
- No need to mess w/ Erlang data types (looking at you charlists)
- Better semantics for process registry
- Documentation on parallel testing options
I think this is a big win.
The one use case where Ruby still wins for me, though, is a little one-off script. Last time I had occasion to do that, I found you couldn't use any Elixir hex packages without generating a whole mix application. In ruby you could just globally install the gem and require it right into your script.
I forget why this came up, but I think it was something like just wanting to download a web page and parse out a certain bit of it into a CSV file or something like that. Pretty trivial in Elixir with HTTPoison and Floki, but no real way to use those in a one-off .exs file (I don't think?). Whereas in ruby, I've already got HTTParty and Nokogiri in there somewhere, and you can toss it together in a minute.
Or you could create a script.rb file, anywhere, and require gems if needed. Then type ruby script.rb.
That takes no time at all, and using gems is extremely simple even with RVM if wanted.
I've been using Elixir for one-off scripts lately and this is something I miss. In my book, it is a clear advantage that Ruby and other scripting languages have over Elixir. In the case of HTTP(S) my solution has been
{data, 0} = System.cmd("curl", ["-sf", url])
but as soon as you need a JSON library it is time to create a project. (The "-f" flag ensures you get a non-zero exit status from cURL if the HTTP status code isn't 2XX.)I do not particularly like the usual global package installation model, though, because it means that you can only have a single version of a package available for your scripts to require. The best package management model for standalone scripts that I have seen is Groovy's [1]: a script can include a special directive that downloads a specific version of a package just for that script. Any versions can be stored in the global (per-user) package cache so that you don't have to redownload them every time a script runs, but your import statements always give you strictly the version you asked for. This is the model I'd adopt for Elixir.
[1] http://docs.groovy-lang.org/latest/html/documentation/grape....
> 4.even?
=> true
That's not to say that what Elixir provides is (or is not) worth giving up everything-is-an-object because Erlang's design does what it is supposed to do very very well.It's a deep subject but one of the biggest things is that it's virtually impossible to end up with the untenable monolith that many ruby apps eventually grow into. Since you aren't dealing with crazy inheritance trees attached to data separating things out really just becomes moving code around.
I do both Ruby and Elixir code right now. There are times when I like the convenience of Ruby for command line scripts or monkey patching a quick modification for a 3rd party library...but there are other times when you hit problems that you just can't solve effectively without rewriting in something else and that gets annoying. That's one of the reasons you see so many posts about moving from Ruby to Go or Elixir because both languages are much better at Ruby's weakest points.
But it's always worth noting that Ruby got those projects to market with maturity and helped them grow customer bases with enough traffic to JUSTIFY a rewrite. What gets people so excited about Elixir is that you lose very little in the development time department for full stack while avoiding the long term complications due to the language rules. All that said, it makes you think about problems differently which will be a challenge for people at first.
4 |> Integer.is_even
Above can be even shortened if you import Integer module: import Integer
(...)
4 |> is_evenWhat Elixir has changed for me, is that I now value the explicitness of my code more than ever. And thanks to this, I aim to write code like this in other languages I use on daily basis (Ruby, Python).
> 4.Integer::even?
=> true
Just chew the syntax a bit, and that's Elixir's: > 4 |> Integer.even?
=> true
Mind you, that has nothing to do with "everything is an object." An object is—in the original Lisp+Smalltalk sense—effectively:1. a stateful closure,
2. which you call with args representing a "message" to send to the object,
4. where the object is free to respond to that message however it likes—i.e. you can code the closure-that-is-the-object however you like—and its responses can depend on its state, meaning that the messages it responds to, and even its internal logic, can mutate over time, as this state changes.
Well, we've got plenty of "objects" in Erlang/Elixir land. We call those "processes." And they are cheap enough that—if you wanted—you could code in an "everything is a process" style. Presuming we defined two modules, ObjectFactory and Object, as GenServers with the requisite functions:
{:ok, obj_f} = ObjectFactory.start_link
foo = obj_f |> ObjectFactory.make_object!
foo |> Object.extend(:bar, fn -> 3 end)
foo |> send(:bar)
# => 3
All Object.extend/2 needs to do its job is to store something in the GenServer's state, and to rely on Object's GenServer.handle_info/2 callback to do something with that state.Now, mind you, this won't magick a Bar module into existence to make this API pretty. But there's nothing stopping you from defining one yourself—or even from creating a Mixin module that you could Kernel.use/1 in your mix-in modules, giving them an API Object.extend/1 could understand, and giving you the ability to:
foo |> Object.extend(Bar)
foo |> Bar.barAll that being said, Ruby (and Smalltalk; and, oddly enough, Javascript) aren't just object-oriented; they go beyond this, into being fully object-based, which is a different thing that Elixir (because of the Erlang VM's architecture) would have a very hard time being.
Ruby—the whole thing—is a graph of objects in a VM, where everything Ruby code does translates to sending a particular message to an object in that graph. The objects themselves start with some responses-to-messages that involve primitives, but if Ruby had a bytecode, it wouldn't contain those primitives, or any way to define them. It'd just be a set of ops to construct values on the stack, and then a send() op. Ruby code effectively boils down to a wire protocol used to speak to a Ruby VM's object graph server. It's all communication, no "client-side" behavior.
So in Ruby, there are no primitives. From the perspective of the VM, 3 might be an unboxed integer primitive; but from the perspective of your Ruby code, it's an object and you have to send messages to it (i.e. to the object-graph server addressed to it.) Ruby code has no way to "operate on" a primitive 3 without asking the object-graph server to do that operating for it.
This is why Ruby "is slow" (and still suboptimal after JIT, due to never being able to drop runtime type checks), but at the same time, it's also why Ruby lets you override Integer#+, and have that effect apply throughout the whole VM. It's why you can re-open a class and define methods on it, when that class is actually a DRb remote object handle. It's why Ruby's Complex and Rational types don't need "first-class" support in order to work with code that never heard of them. Everything is a black box, and so everything is forced to be message-sends to those black boxes. There are no primitives, and so there's no code that expects primitives, which you could break by defying that expectation.
---
As well, Ruby, like Smalltalk, and like Javascript, is image-based: these are languages without an abstract-machine primitive to "load a module." There are no modules. There is only the object graph.
"Library" code in these languages isn't in a "dead" form that gets dumped into VM memory and linked in with symbolic references; instead, it's the source code to a program, a program that gets evaluated when require()d. This program is much like an SQL migration: it can do anything regular code can do, but it usually sends one or more messages to the object-factory-factory member of the graph (i.e. Ruby's Class) asking it to make a new object-factory; and then sends messages to that object-factory, asking it to add mappings from some passed symbol values, to some passed lambda values (i.e. to define some instance methods.)
Smalltalk is a bit different in that Smalltalk has "image-based persistence", meaning the VM can hibernate, and so there's less infrastructure around "loading a bunch of libraries in a tree at VM startup", and more infrastructure around "purging the effects of old libraries and loading new ones." Less like an OS boot process; more like an OS with a package-manager that can install/uninstall packages that start/stop services. But fundamentally, it's the same idea: Smalltalk source code is a migration that executes on the Smalltalk VM to mutate the [persistent] VM object graph.
I think the former encompasses the true essence of what Alan Kay have stated when he coined the term OOP. Joe Armstrong and Alan Kay had a talk about how Erlang is more OOP than most languages.
Classes in general hurt.
Someone mentioned that Ruby is more useful for scripts because libraries can be installed and used at the system level; that seems true to me, so that's one example. But I think the main tradeoff that Ruby wins on is maturity. Which is a big deal to be sure.
There's no reason Elixir can't do this. Or rather, for interactive scripting under iex(1) without a project scope, there's nothing stopping someone from adding a feature that
1. assumes some default project scope (e.g. "~/.mix/default"), and
2. allows you—even from within the REPL!—to add a dependency to "~/.mix/default/mix.exs", re-run the dependency constraint solver, and (if successful) compile + reload the Mix project, or (if failed) back out the mix.exs change.
On the other side, for the use-case of "scripts as standalone redistributable binaries", I prefer Elixir's philosophy of "everything is vendored, always"—the [redistributable form of the] script just has all its deps inside it, much like SPA Javascript apps do.
It's indeed true, though, that you can't achieve a hybrid of these use-cases: have your own personal set of IEx libraries loaded and able to be used to probe and debug within an existing Mix project that contains its own set of libraries, where these libraries' dep specifications—if combined in a single Mixfile—wouldn't constraint-solve. That is an annoying problem that I run into quite often, especially because Elixir doesn't ship with many tools I'd like to use during debugging, like a JSON library, and I end up having to alter the project's own Mixfile to add these deps, rather than just "bringing in" my own.
At root, that problem is a semi-architectural problem with the Erlang VM: modules are indexed by [fully qualified] name, so—unlike in Javascript—you can't have "handles to" two modules with the same name. (But, actually, there's nothing about the design of Erlang or Elixir that requires the Erlang VM to work that way; it could be fixed, if people thought it'd be helpful.)
Crystal's advantages over Elixir would be static typing, compile time performance, binaries, and having something like 80 percent of Ruby's syntax. The last one is for people who do like Ruby as a language. Elixir is only superficially similar to Ruby in some syntactic ways.
Fortunately, Rustler is making adding safe native extensions much easier. https://github.com/ddresselhaus/primal_ex is a little toy library I wrote to leverage a really excellent Primal Rust library. I'm excited about the possibilities Rustler brings to the table.
"You gain a lot over Ruby and lose very little."
"...better in almost every way."
These are very shortsighted assertions. As engineers we should strive be a little more objective and at least mention pros and cons of both choices.
For example, is ease of deployment important to you and your project? How do these languages compare there? Try not to get blinded by the honeymoon phase with a language.
This means deployment and running a production elixir app is fundamentally "different", which leads to people reaching for hot code swapping, treating servers as "pet" instead of "livestock", etc.
With tools like Kubernetes and containers taking over more and more of the ops world, running an Elixir application feels like swimming against the current as the BEAM VM have its own clustering system that requires some workaround to make it work well with an elastic auto-scaling cluster.
They know where the skeletons are and avoid things that are bad ideas in that are if possible.