Hacker News new | past | comments | ask | show | jobs | submit login
Ruby vs Elixir – Panel discussion at Wroc_love.rb [video] (youtube.com)
75 points by jschepmans on April 15, 2017 | hide | past | favorite | 45 comments



I think it comes down to this: with Elixir, you gain a lot over Ruby and lose very little. Any Elixir app will be significantly more performant and reliable than its Ruby counterpart. This is due to several reasons:

- 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).


It seemed like in the panel they just kept looping back to the performance and concurrency, to which the Ruby guys would say "So what, Ruby is fast enough for 99% of apps", completely ignoring all of the other strengths of Elixir.


Because Elixir is better than Ruby in webapp space, in term of speed, you don't need to reach for caching and complex backend solution as early as RoR.

I think this is a big win.


Former Ruby, now Elixir, developer here and I agree.

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.


I am currently working on a RoR project and I am looking at Elixir for a side project. Kindly advice how I can get started in Elixir.


There is a tutorial right on the official website. I would start with this, it covers quite a lot of ground.


>In ruby you could just globally install the gem and require it right into your script.

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....


Mix tasks are what you're looking for. You put them inside your application (we have them in /appa/your_app /lib/mix/tasks), define a run/1 function that takes in args, say "use Mix.Task", then you can make sure that things like Ecto are loaded and interact with your program's major entities, or use Poison or whatever it is that you care to do.


....

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.


That works for quick and dirty scripts but it's not portable if you want to distribute it to others.


I like Ruby for one off scripts, too. In some cases though, I don't like having to install Ruby. If Elixir, or perhaps Erlang, is on the target box, an Elixir Escript is also a good option. I've picked up a fair bit of bash for easier tasks, too.


I love Elixir, and came from Ruby. I do find it hard to describe to people why I like it more it more. There is no one thing that is a killer feature to me. I do not miss inheritance at all.


IMO, Elixir appeals to developers who have experienced the challenges of the established code bases because you immediately realize the long term benefits of the way it's designed. I've noticed a pattern at least that it seems to appeal to people with more experience because it's easier to recognize what the little details bring.


Pattern matching, to me, is the killer feature. It allows me to write so many fewer if statements, which to me is a big boon.


Here's a list of things Elixir things I love:

- 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


As far as Dialyzer, we use the dialyxir project, and it behaves well enough. Depending on how macro heavy your code gets, it gives up earlier than it should, but you can still get it to flag you for doing bad things when it gets down to modules calling modules. Combined with lint rules around making sure stuff has @spec, it catches a lot of things.


I use dialyxir, too. Mostly I was referring to wanting dialyzer to output Elixir syntax, not Erlang.


My biggest issue with Elixir right now is that in order to take advantage of its main selling points (process model, concurrency, etc), your running applications are essentially stateful. This adds some complexity to the way it is run, especially if the server load is highly dynamic, and you need to scale up and down instances every few hours.

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.


Elixir and Ruby are so different, they shouldn't even be allowed to be compared. If you don't think so, try and write some medium-sized code challenges in both and see how different the implementations come out (try more than 3 because sometimes you can express the same idea in both in a similar way).

"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.


It's different, but I'm not sure that makes it better than Ruby. You listed three advantages for Elixir. Are there none for Ruby? Language design involves tradeoffs. Are there no tradeoffs for being immutable and functional?


You're right that there are always tradeoffs, but it isn't clear that Ruby takes the other side of many of Elixir's. Some examples: Elixir requires a VM installed on the target machine (unlike Go, C, C++, etc.), it doesn't have strong type checking (unlike Java, Scala, Haskell, etc.), it doesn't have a strong data science story (unlike Python and R), it can't be conveniently used as a dual front- and back-end language (unlike javascript, TypeScript, etc.). But Ruby is on the same side of all those tradeoffs.

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.


Maybe an Elixir vs Crystal discussion for ex-Ruby programmers would be appropriate.

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.


> Ruby is more useful for scripts because libraries can be installed and used at the system level

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.)


For the limited number of mathy things I've done in Elixir, performance has been pretty terribly. Ruby is a bigger language which means it's more likely to have C binding for things like generating prime numbers, for example.

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.


That's not entirely accurate. An Elixir release compiled for the target platform can be bundled up in a tarball with everything it needs to run, including the runtime. No Elixir install required. I do agree that pushing around a single binary in general is easier, all other things being equal.


Playing around with Elixir, it seems like it loses Ruby's Smalltalk like "everything is an object" that allows things like:

  > 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.


There's basically data and functions, so 4 wouldn't really have any methods attached to it and instead you'd just call Number.even?(4). Purely psuedo code example.

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.


Or like this, thanks to the pipe operator

  4 |> Integer.is_even
Above can be even shortened if you import Integer module:

  import Integer
  
  (...)

  4 |> is_even


Collectively at work, we've abandoned the use of import, except when importing DSLs (e.g. Ecto) in favor of explicit function calls or alias. If we do need to use import, we use explicit "import Ecto.Query, only: [from: 2]", so that it's clear where a function is coming from into the namespace. Relatedly, unless otherwise necessary, __using__ macros through our code do not import modules, because it's surprising when something is in scope for no real reason. We often inline the third party __using__ macros to eliminate this.


And I completely agree with this, I have just posted the above example to show that Elixir's syntax can be concise too in case anyone would want that. :)

What 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).


For sure. I just think a lot of new people get caught up in the ability to be concise for self that they forget about the expressiveness for others. Unrelated, we also abandoned 1 pipe pipelines, and instead prefer "function(arg)". Just looks better to us, despite the minor maintenance cost of having to change it if you convert to multiple pipes.


Imagine Ruby, if you had to explicitly namespace every method call with the mix-in that contains the method, so that 1. there could never be collisions between method names, and 2. finding mix-in method definitions at compile time was context-free. It'd maybe look a bit like this:

    > 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.bar


(Replying to myself for the sake of readability:)

All 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.


Elixir isn't an object-oriented language.


alternatively it is the most object oriented language


Haha, nice observation. But not really. The only facet of object orientation is processes communicating with message-passing semantics. Primitive types are not objects, and processes are not instances of a class for example.


But you can get "polymorphism" by spawning different processes that all respond to the same messages. So in that sense the process is the "instance", and the process function is the "class".


I'll take "everything is an object if you represent it as a process" over Ruby's everything is an object.

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.


All that being said, Ruby is much more like Smalltalk than Elixir or Erlang are. Smalltalk does have inheritance and classes, and does the kind of meta stuff easily that Ruby supports. Objects and classes are as central to Smalltalk's design as message passing is.


To touch on what digitalzombie said however, Kay did not originally picture inheritance as being part of OOP.


He is even against it and made it clear a couple time.

Classes in general hurt.


There is a thing that was missed. You may have developed an application very quickly with ruby and go to market faster in 6 months, but you spend the next 5 years trying to fix performance issues with barely writing any new features.


I am falling for Elixir. However, my big drawback that made me stay in Ruby is the community don't seem to like NoSQL solutions like MongoDB and you can feel it in the libraries and global architecture.


It's just that they have their own ways of expressing it, using things like ETS, DETS, mnesia, Riak-core, depending on your use cases and consistency requirements.


Because the erlang community (and core member of the elixir community) have quite a lot of experience with distributed systems and alternative to SQL.

They know where the skeletons are and avoid things that are bad ideas in that are if possible.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: