DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

100 Languages Speedrun: Episode 81: Elixir

Erlang is a platform with some powerful distributed computing capabilities, but the language Erlang it uses is really awful. Everything about Erlang the language, its syntax, its standard library, its string handling, its Unix integration, and so on, is a huge pain.

In a way it's a similar to the situation with Java and JVM, but for all the fashionable hate against it, Java the language is nowhere near as bad as Erlang the language.

So some good people did the right thing, and created Elixir - a much better language for the same platform. This was far more successful than efforts to replace Java, and according to StackOverflow surveys, Elixir is currently more than twice as popular as Erlang, and became the main language on the Erlang VM.

Meanwhile, Kotlin and others still have long way to go to overcome the Java menace - even taken together all the non-Java JVM languages don't constitute even 50% of the JVM world.

Elixir has Ruby-inspired syntax, but it's mostly surface level similarity, and the languages are very different.

I also need to make a small correction, as back in the Lua episode I said that Lua is pretty much the only significant tech that came out of Brazil. Elixir is another fairly successful Brazilian language.

As Foretold

Back in 2006 I wrote an Erlang review, somewhat similar to what I'm now doing for this series, where I said this:

Here's my proposal:

  • Conform to the basic Unix conventions like ^D, man pages, and --help.
  • Throw away the old syntax and add something Python/Ruby-like. This isn't Lisp - weird syntax doesn't give you anything. Writing a decent parser in ANTLR is just one evening, and you don't have to throw away the old syntax, just provide an alternative. When you're at changing syntax, make it possible to access full language from the interpreter and limit the repeating yourself part a bit.
  • Write a decent standard library - real strings with Unicode (not this lists of integers hackery), regular expressions, arrays, hash tables and so on.
  • There is absolutely nothing that forces compilation by hand. Make it automatic by default.

Such changes won't interfere with the fault-safe distributed computing part even the tiniest bit. And if Erlang stays the way it is now, I think it is extremely unlikely to get out of its telecom niche.

And it turns out I was right to a prophetic degree. Elixir released 6 years after I wrote that, delivered the whole wishlist except ^D, and it's far more popular than Erlang ever was. It's nice to be on the right side of history.

Hello, World!

Let's start with the usual. Here's Hello, World! in Elixir:

#!/usr/bin/env elixir

IO.puts("Hello, World!")
Enter fullscreen mode Exit fullscreen mode
$ ./hello.ex
Hello, World!
Enter fullscreen mode Exit fullscreen mode

REPL

Elixir has REPL, but it's not amazing. Ctrl-D doesn't work at all, and if we use Ctrl-C as tells us, it goes to this weird dialog:

$ iex
Erlang/OTP 24 [erts-12.2] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit] [dtrace]

Interactive Elixir (1.13.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 2+2
4
iex(2)> ^C
BREAK: (a)bort (A)bort with dump (c)ontinue (p)roc info (i)nfo
       (l)oaded (v)ersion (k)ill (D)b-tables (d)istribution
a
Enter fullscreen mode Exit fullscreen mode

Unicode

If you don't look too close, Elixir seems to support Unicode just fine:

#!/usr/bin/env elixir

IO.puts(String.length("Hello"))
IO.puts(String.length("Żółw"))
IO.puts(String.length("🍰"))
IO.puts(String.upcase("Żółw"))
IO.puts(String.downcase("Żółw"))
Enter fullscreen mode Exit fullscreen mode
$ ./unicode.ex
5
4
1
ŻÓŁW
żółw
Enter fullscreen mode Exit fullscreen mode

Strings and Erlang VM

But under the surface, the string situation on Erlang VM is a huge mess. There are many "string"-like types, including:

  • linked lists of integers - with no way to indicate which are supposed to be "strings" and which aren't - this is the main "string" type of Erlang
  • BitStrings with various encodings
  • atoms (like Ruby Symbols)

In Erlang the language, it's a total disaster. Here's some attempts at creating a simple non-ASCII string in Erlang REPL:

1> <<"Żółw">>.
<<"{óBw">>
2> <<"Żółw"/utf8>>.
<<197,187,195,179,197,130,119>>
3> "Żółw".
[379,243,322,119]
4> 'Żółw'.
'Żółw'
Enter fullscreen mode Exit fullscreen mode

That last one that seems closest to working is actually an atom (Symbol), not a String.

Elixir does its best to make this better. It makes all strings into UTF8 BitStrings by default, as it's the most reasonable of the available options, adds proper input and output, so you can mostly use Elixir as if it had proper string support.

Sometimes you'll run into confusing string issue due to the underlying Erlang VM just not being fully on board with this:

iex(1)> [10,20]
[10, 20]
iex(2)> [10]
'\n'
iex(3)> [60,70,80]
'<FP'
iex(4)> 'Żółw'
[379, 243, 322, 119]
Enter fullscreen mode Exit fullscreen mode

Some lists of numbers are printed as lists of numbers, some as "strings", based on their content. And it's not the same rules for REPL and IO.puts. Overall Elixir goes really far towards fixing the Erlang VM string mess, even if it can't quite 100% fix it.

FizzBuzz

Let's do the FizzBuzz!

#!/usr/bin/env elixir

defmodule FizzBuzz do
  def fizzbuzz(n) do
    cond do
      rem(n, 15) == 0 -> "FizzBuzz"
      rem(n, 3) == 0 -> "Fizz"
      rem(n, 5) == 0 -> "Buzz"
      true -> n
    end
  end

  def loop(range) do
    range |> Enum.map(&fizzbuzz/1) |> Enum.each(&IO.puts/1)
  end
end

1..100 |> FizzBuzz.loop
Enter fullscreen mode Exit fullscreen mode

The syntax looks vaguely Ruby-like, but none of the details are quite the same. The thing about Elixir people seem to love the most is the |> operator, which already made its way into some other languages like Julia. a |> b is just b(a), but if the data is flowing through a bunch of things, a |> b |> c |> d is usually a lot more readable than d(c(b(a))), or using intermediate variables.

This only works if the function you pipe into has the right argument at the right position. You can pipe into foo(a, _), but not so much into foo(_, a). For anything else you'll need to use anonymous functions or some other more complicated syntax.

Highly Object-Oriented language like Ruby has less need for |> as . already does a lot of the same, also only for the argument in the correct position (in this case the self argument). Elixir's range |> Enum.map(&fizzbuzz/1) |> Enum.each(&IO.puts/1) would translate into Ruby's range.map{|x| fizzbuzz x}.each{|x| puts x}. The match between |> chaining and . chaining isn't exact, but they cover a lot of similar situations.

And of course people keep asking for more, in both languages, and in all other languages with pipelining.

And the details, step by step:

  • all functions must go inside modules, so we need that defmodule FizzBuzz do ... end - this is just annoying, and Elixir really should just allow top level and REPL-level defs.
  • do ... end use doesn't quite match Ruby convention, there's a lot more dos than in Ruby. You can also use do: ... for do ... end.
  • functions all have specific "arity" (number of arguments they take), so we needed to say &fizzbuzz/1 to refer to fizzbuzz(n), we can't just say &fizzbuzz (as that would mean fizzbuzz()).
  • there's if and if/else, but no elsif, which is overalll very unusual; cond does this just as well
  • you mostly can't modify variables

Processes

Let's try to do something with processes. We'll spawn a process to green people, and send it a bunch of messages with who we'd like it to greet.

The greeting process will also keep a greeting counter.

#!/usr/bin/env elixir

defmodule Greeter do
  def loop(counter) do
    receive do
      {:hello, msg} -> IO.puts("#{counter}: Hello, #{msg}!")
    end
    loop(counter + 1)
  end
end

greeter_pid = spawn(fn -> Greeter.loop(1) end)
send(greeter_pid, {:hello, "World"})
send(greeter_pid, {:hello, "Alice"})
send(greeter_pid, {:hello, "Bob"})
send(greeter_pid, {:hello, "Eve"})
Enter fullscreen mode Exit fullscreen mode
$  ./processes.ex
1: Hello, World!
2: Hello, Alice!
3: Hello, Bob!
4: Hello, Eve!
Enter fullscreen mode Exit fullscreen mode

What's going on here:

  • spawn creates a new process, returning a PID (process ID)
  • we can send messages (tuples) to that process with send - we can use its PID, or some other identifier
  • the main way to maintain state is with function arguments - if you want to update the state, just call yourself with updated arguments, that's what loop(counter + 1) does
  • receive do ... end takes one message from the process's mailbox, and does something based on it
  • in this case we only care about {:hello, msg} message, and what we do is print the hello, with a counter

Accounts

Let's do something a bit more complicated with processes.

There will be Account processes maintaining someone's account, with these operations:

  • Account.create(name, initial_balance)
  • {:deposit, value}
  • {:withdraw, value}

And Bank process:

  • Bank.create
  • {:create_account, name, initial_balance} message
  • {:transfer, from_name, to_name, amount} message
#!/usr/bin/env elixir

defmodule Account do
  def loop(name, balance) do
    new_balance = receive do
      {:deposit, value} -> balance + value
      {:withdraw, value} -> balance - value
    end
    IO.puts("Balance for #{name} changed from #{balance} to #{new_balance}")
    loop(name, new_balance)
  end

  def create(name, initial_balance) do
    IO.puts("Account created for #{name} with initial balance #{initial_balance}")
    loop(name, initial_balance)
  end
end

defmodule Bank do
  def transfer(map, from_name, to_name, amount) do
    send(Map.get(map, from_name), {:withdraw, amount})
    send(Map.get(map, to_name), {:deposit, amount})
    loop(map)
  end

  def create_account(map, name, initial_balance) do
    pid = spawn(Account, :create, [name, initial_balance])
    map = Map.put(map, name, pid)
    loop(map)
  end

  def loop(map) do
    receive do
      {:create_account, name, initial_balance}
        -> create_account(map, name, initial_balance)
      {:transfer, from_name, to_name, amount}
        -> transfer(map, from_name, to_name, amount)
    end
  end

  def create do
    loop(%{})
  end
end

bank = spawn(Bank, :create, [])

send(bank, {:create_account, "Alice", 1000})
send(bank, {:create_account, "Bob", 2000})
send(bank, {:create_account, "Eve", 200})
send(bank, {:transfer, "Alice", "Bob", 500})
send(bank, {:transfer, "Bob", "Eve", 220})
Enter fullscreen mode Exit fullscreen mode

It's all far more async than any mainstream language. Notice how there's no guarantee these will happen in any meaningful order:

$ ./accounts.ex
Account created for Alice with initial balance 1000
Account created for Eve with initial balance 200
Account created for Bob with initial balance 2000
Balance for Eve changed from 200 to 420
Balance for Bob changed from 2000 to 2500
Balance for Alice changed from 1000 to 500
Balance for Bob changed from 2500 to 2280
$ ./accounts.ex
Account created for Bob with initial balance 2000
Account created for Eve with initial balance 200
Account created for Alice with initial balance 1000
Balance for Alice changed from 1000 to 500
Balance for Bob changed from 2000 to 2500
Balance for Eve changed from 200 to 420
Balance for Bob changed from 2500 to 2280
Enter fullscreen mode Exit fullscreen mode

Processes are spawned and messages are sent, but when they get processed is a mystery, and you need to be prepared for that.

Some of the details:

  • Account - whole state, both constant (name) and variable (balance) exists as arguments to loop - to update the balance, it calls itself with new argument
  • Bank - the only state is map from names to Account process IDs; account balances only exist in Account processes. If Bank wanted to know them, Account would need to implement more operations like (get_balance) and it would need to ping the accounts. Of course with everything being async, that wouldn't necessarily be the final state, maybe some messages are still going?
  • spawn(Account, :create, [name, initial_balance]) is another syntax for spawn(fn -> Account.create(name, initial_balance) end), without extra anonymous function

Fibonacci

Here's a fun Fibonacci implementation with processes all the way.

If it goes well, the fib(20) process receives messages {:fib, 18, 2584} and {:fib, 19, 4181}, and then it prints what it calculated, and sends its result as message {:fib, 20, 6765} to processes fib21 and fib22 so the chain can all continue.

However, this code is not quite correct, to demonstrate how just how async Elixir is.

Process.register is a way to associate symbol names with processes, and then you can send messages to such a name, just as you can send them to a Process ID. But because everything is async, we can't be sure the process is actually registered, so we have a fun little race condition here.

As an fun little exercise for the reader, what extra steps would need to be taken to remove the race condition? And no, spawning the processes backwards (40..1) is not the answer.

#!/usr/bin/env elixir

defmodule Fib do
  def done(n, value) do
    IO.puts("fib(#{n}) = #{value}")
    send(:"fib#{n+1}", {:fib, n, value})
    send(:"fib#{n+2}", {:fib, n, value})
  end

  def waitforprevious(n, a, b) do
    {a, b} = receive do
      {:fib, m, value} -> cond do
        m == n - 1 -> {value, b}
        m == n - 2 -> {a, value}
        true -> {a, b}
      end
    end

    if (a == 0 or b == 0) do
      waitforprevious(n, a, b)
    else
      done(n, a + b)
    end
  end

  def calculate(n) do
    if n <= 2 do
      done(n, 1)
    else
      waitforprevious(n, 0, 0)
    end
  end
end

(1..40) |> Enum.each(fn n ->
  Process.register(spawn(Fib, :calculate, [n]), :"fib#{n}")
end)
Enter fullscreen mode Exit fullscreen mode

One of the things can happen - either we got hit by the race condition or we did not:

$ ./fib.ex
fib(1) = 1
fib(2) = 1
$ ./fib.ex
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
fib(10) = 55
fib(11) = 89
fib(12) = 144
fib(13) = 233
fib(14) = 377
fib(15) = 610
fib(16) = 987
fib(17) = 1597
fib(18) = 2584
fib(19) = 4181
fib(20) = 6765
fib(21) = 10946
fib(22) = 17711
fib(23) = 28657
fib(24) = 46368
fib(25) = 75025
fib(26) = 121393
fib(27) = 196418
fib(28) = 317811
fib(29) = 514229
fib(30) = 832040
fib(31) = 1346269
fib(32) = 2178309
fib(33) = 3524578
fib(34) = 5702887
fib(35) = 9227465
fib(36) = 14930352
fib(37) = 24157817
fib(38) = 39088169
fib(39) = 63245986
fib(40) = 102334155
Enter fullscreen mode Exit fullscreen mode

Oh in the real world you'd likely mostly use higher level features, not just do everything with spawn and send and throwing PIDs around like I'm doing it here.

Should you use Elixir?

If you're wondering Elixir vs Erlang, that's not even a real contest. Never use Erlang, Elixir is just strictly better in every conceivable way.

As for the question if you should be using Erlang VM or not, it definitely offers a very unique concurrency model, and leads to software organized in ways that can't really be done on any other platform. I don't think all that many problems need this concurrency model, but if you do, Elixir gives you access to all these capabilities in a fairly decent language.

Code

All code examples for the series will be in this repository.

Code for the Elixir episode is available here.

Top comments (0)