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!")
$ ./hello.ex
Hello, World!
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
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"))
$ ./unicode.ex
5
4
1
ŻÓŁW
żółw
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'
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]
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
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-leveldef
s. -
do ... end
use doesn't quite match Ruby convention, there's a lot moredo
s than in Ruby. You can also usedo: ...
fordo ... end
. - functions all have specific "arity" (number of arguments they take), so we needed to say
&fizzbuzz/1
to refer tofizzbuzz(n)
, we can't just say&fizzbuzz
(as that would meanfizzbuzz()
). - there's
if
andif/else
, but noelsif
, 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"})
$ ./processes.ex
1: Hello, World!
2: Hello, Alice!
3: Hello, Bob!
4: Hello, Eve!
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})
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
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 toloop
- 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. IfBank
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 forspawn(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)
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
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.
Top comments (0)