Erlang for Rubyists
Disclaimer: I’m in the process of learning Erlang.
Projects built with Erlang include Amazon SimpleDB, RabbitMQ, and CouchDB.
Installing
brew install erlang
Hello, world
-module(hello).
-export([hello/0]).
hello() ->
io:write("Hello, world!~n", []).
module
says that everything in this file belong to the hello
module. The module provides a namespace for a group of functions much like a Module
in Ruby.
Erlang modules, unlike in Ruby, must start with a lowercase letter.
hello()
is a function, this is the equivalent of a method in Ruby.
The export
describes which functions can be called externally, it is the same idea as public
methods in a Ruby class. Therefore by default functions in a Erlang modules are private and must be explicitly exported to be visible from outside the module.
The square brackets in the export
indicate a list, like an Array in Ruby. The one and only element in there hello/0
exports the hello
function with zero arguments. In Erlang functions of the same name can be declared multiple times but different number of arguments know as the arity of the function.
The actual function hello()
starts with ->
, like def
in Ruby and ends with a period, like end
in Ruby. Note that every line ends in a period.
In the body of the function we call the write
function in the io
module. The io
module is part of the Erlang standard library known for historical reasons as OTP (Open Telecom Platform).
io.write
is the same as puts "hello, word\n"
. In Ruby put
is defined in the Kernel
module (which is mixed in to every object) so it is shorthand for Kernel.puts
in Ruby.
The equivalent, verbosely written, in Ruby is:
module Hello
public
def self.hello
Kernel.puts "hello, world\n"
end
end
The difference with Erlang modules is that they only provide namespacing, they can’t be mixed in to an object, since there are no objects.
Like in Ruby Erlang will return the value of the last line in a method/function.
Variables in Erlang are scoped to the function in which they are declared. There is no such thing as an instance variable (like @firstname
) in Erlang. There is no global state either.
Once assigned a variable in Erlang can never be reassigned. It, unlike in Ruby, is immutable. The equivalent in Ruby would be to freeze
a variable after assigning it:
name = 'Kris'.freeze
name = 'Chris' # error
In Erlang variables must start with an uppercase letter. So modules lowercase and variables uppercase, the reverse of Ruby.
X = 1.
X = 2. # => error
You can try the above out in the Erlang console, erl
.
When a variable is assigned it is said to be bound.
The lack of mutation and global state is part of the reason Erlang can scale so well. Nothing is shared.
Pattern matching
Pattern matching is one of the key features of Erlang (and other functional languages).
Pattern matching is a form of message dispatch based on the name and arity functions. It is similar to the idea of overloading a method. Overloading a method not possible in Ruby but can be faked using a splated argument.
def run(*args)
if args[0].is_a?(Hash)
# ...
else
# ...
end
end
However pattern matching takes overloading one step further and allows us to select a function not just on its name, not just the number of arguments, but also the values of those arguments.
If Ruby had pattern matching we could do this:
def fibo(0)
0
end
def fibo(1)
1
end
def fibo(n)
n > 0 ? fibo(n-1) + fibo(n-2) : n
end
And depending on the value passed to fib
it would dispatch to the matching method.
In reality we have to do something like this:
def fibo(n)
return 0 if n == 0
return 1 if n == 1
fibo(n-1) + fibo(n+1)
end
The previous hypothetical version is more readable to my eyes. Testability is also potentially improved as our code is broken down in to smaller units.
In Erlang, with pattern matching, we can do this:
-module(fib).
-export([fib/1]).
fib(0) -> 0;
fib(1) -> 1;
fib(N) -> fib(N-1) + fib(N-2).
This looks similar to our hypothetical Ruby version.
Notice that in the export
we have fib/1
, that exports the fib
function which has an arity of 1.
Pattern matching is also used when using the equals sign. Erlang treats the equals sign in two different ways depending on if the var has already been assigned or not. When the variable has not yet been assigned it is assigned, or bound. But if you use the equals sign on a bound var it acts more like an assertion, i.e. ==
in Ruby, but is raises an error if the the result is not true
. This is due to pattern matching.
X = 1. # binds X to 1
X = 1. # no error
X = X. # no error
X = 3. # error
As we can see var’s once assigned are immutable, this is a side effect of pattern matching. The last line X=3
does not match since X
equals 1
, thus an error is raised. Here is another example.
Me = { person, 'Kris', 'Leech' }.
{ human, Firstname, Lastname } = Me. # error
{ person, Firstname, Lastname } = Me.
FirstName. # => 'Kris'
On line two the pattern does not match and an error is raised.
Notice that in the third line, because the tuples match, assignment occurs for the Firstname
and Lastname
variables.
In our case we wanted to make sure that we only assigned if the first element of Me
is the atom person
. If we did not care about the first element we could have done this { _, Firstname, Lastname } = Me.
. This would assign Firstname
and Lastname
regardless of the first arguments value.
The underscore is used when we don’t care about an argument, this is the same in Ruby when using blocks.
{..}.each do |k,_|
# ...
end
Lists, Tuples and Atoms
Tuples, {1,2,3}
are like fixed size arrays, there is no real equivalent in Ruby.
Lists, [1,2,3]
, are like dynamically sized arrays, the same as Array
in Ruby.
A List has a a tail and head, like [1,2,3].first
and [1,2,3].last
in Ruby. Except Tail
and Head
can also be used in pattern matching, for example:
[Head|Tail] = [1,2,3,4].
Head. # => 1
Tail. # => [2,3,4]
Note that Head
and Tail
are variables and could have been named something else, e.g. [First|Rest]
.
In Ruby we can do something similar with a splat:
head, *tail = [1,2,3,4]
head # => 1
tail # => [2,3,4]
In Erlang lists can also be concatenated with ++
:
[1,2,3] ++ [4,5,6]. # => [1,2,3,4,5,6]
And in Ruby:
[1,2,3] + [4,5,6]
An atom is like a symbol in Ruby. An atom must start with a lowercase letter. Since there are no objects or types it is common to use a tuple where the first element is an atom such as: { person, ‘Kris’, ‘Leech’ }
or {point, 100, 240 }
.
Unlike in recent versions of Ruby atoms are not garbage collected so should never be generated from user input to avoid using up all memory.
Guards
Guards are clauses that can be added to a function to restrict pattern matching. For a match to occur the pattern must match and the guard must pass.
Guards are implemented with the when
keyword. In this example we use a guard clause to check the input is within a range. In regular pattern matching ranges are not supported so we must use a guard.
is_in_range(X) when X >= 22 ->
true;
is_in_range(_) ->
false.
Given is_in_range(3)
the first matching function will be checked, the guard fails, and the next matching function is tried. In this case we don’t care what the given value is because we know if it didn’t pass the guard in the first function it must fail.
Note that in this case we need to use ;
after the first function instead of .
. You can think of ;
a bit like else
in Ruby.
You can add multiple guards which are comma separated, e.g. when X >= 22, X <= 44
.
There is no equivalent in Ruby since we do not have pattern matching, the closest would be to raise if a condition on the given arguments is not met:
def is_in_range(input)
raise ArgumentError unless input >= 22
# ...
end
if statements
is_it_okay(X) ->
if X == 1 -> true;
true -> false
end.
The syntax is a little odd and similar to if/elsif/else
in Ruby. In Erlang every expression has to return something, but unlike in Ruby you have to explicitly return a value from an if
. This is the true
part. If none of the if
clauses match the true
(like else
in a Ruby case
statement) will be called.
You only need true
if it is possible that X
might not match one of your if
statements. For example:
is_it_okay(X) ->
if X > 1 -> true;
X < 1 -> false
end.
Concurrency
Erlang is said to be concurrency oriented programming language (COPL).
In Ruby we might start a new OS process or a thread, or perhaps use a gem like celluloid which implements the Actor pattern.
Thread.new do
end
In Ruby global state can be shared between threads and we need to synchronise access via locks to variables when we access them. For example constants, including classes and Singelton objects.
def initialize
@mutex = Mutex.new
end
def call(name)
with_mutex { @name ||= name }
end
private
def with_mutex
end
In Erlang there are process and no state is shared between them. In fact in Erlang in general there is no global state at all.
Process in Erlang do not map to OS process but are created, managed and scheduled by the Erlang VM. They are much more light weight than OS processes, using much less memory. The first 2,500 processes each take just 1µs to start. Context switching is much faster than OS processes.
There is no state shared between Erlang processes, they are totally isolated. Sharing nothing removes many issues normally associated with concurrency.
Processes in Erlang are like objects in Ruby except they have no state.
Any exported function can be run in an Erlang process.
Pid = spawn(test, hello, []).
hello() ->
io:fwrite(“in a new process”, []).
spawn
returns a process id.
We can send messages between process. It takes around 0.8µs to send a message between two processes.
Like Celluloid, Erlang implements the Actor pattern. Each process has a mailbox which is a queue of messages ready for processing.
Messages are fire and forget. There is no return value, since they are asyncronuse.
Messages sent to a process are simply bits of data, strings, integers, lists or turtles.
Typically a tuple where the first element is an atom are sent, the following arguments are the data.
In Ruby
[:event_cancelled, 3]
The first element is a symbol, the name of an event which has occurred, the second a event id.
``erlang {event_cancelled, 3}
The first element is an atom, the name of an event which has occurred, the second a event id.
A message is sent to a process as such:
erlang Pid ! {:event_cancelled, 3}
The receiving process uses pattern matching to decide which function to handle the message.
erlang run() -> receive ping -> io:fwrite(“pong”), []) after 2000 -> io:fwrite(“Timeout”)
```
The after
keyword determines how long the process will stay live before terminating.
Processes can self terminate or be terminated by other processes which know the pid.
Processes can send messages to processes on other machines.
References: