When I tell my fellow software engineers that I work with Erlang, they look either puzzled, curious, or both. Erlang isn't very well known, because it's not a popular general-purpose programming language. But that doesn't mean Erlang isn't used.
What is Erlang?
Companies like Cisco, WhatsApp, and Ericsson use Erlang because of its scalability, fault tolerance, and high availability. So despite Erlang's relative obscurity among programmers, billions of people interact with apps that are at least partly made with Erlang code.
If I were to give a more formal definition, I'd say that Erlang is a garbage-collected, functional programming language that runs in a VM called BEAM. You'll often see Erlang associated with OTP (Open Telecom Platform), the acronym for the whole Erlang ecosystem that includes:
- The BEAM run-time system (interpreter and compiler)
- Erlang protocols
- Several Erlang libraries (some written in Erlang and some in C)
5 Interesting Features of Erlang
1. Pattern Matching
Like most other functional languages, Erlang uses pattern matching. This means that you cannot change a variable's value after it has been assigned. For example, if you say x=1
, you cannot then say x=2
(although you can say x=2-1
). Once a variable is assigned, the =
operator is used for pattern matching. If there's no match, you'll get an error:
1> X = 1.
1
2> X = 2.
** exception error: no match of right hand side value 2
3> X = 2 - 1.
You can also use pattern matching in function headers, so that your software can take different actions when going through functions:
-spec week_day(non_neg_integer()) -> binary() | {error, invalid_day}.
week_day(1) -> <<"Sunday">>;
week_day(2) -> <<"Monday">>;
week_day(3) -> <<"Tuesday">>;
week_day(4) -> <<"Wednesday">>;
week_day(5) -> <<"Thursday">>;
week_day(6) -> <<"Friday">>;
week_day(7) -> <<"Saturday">>;
week_day(_) -> {error, invalid_day}.
Note that the use of _
before a variable name is a catch-all handler. You can see it as an "else" clause in the function. Use it to partially assign values from a tuple to a variable. Here's how I use it to get only the current hour value from the erlang:time/0
response:
%% erlang:time/0 returns a tuple with three elements representing the current time: {Hour :: integer(), Minute :: integer(), Second :: integer()}
1> {Hour, _Minute, _Second} = erlang:time().
{14,16,27}
2> Hour.
14
See how Minute
and Second
were ignored and not assigned a variable? That's how _
works.
When pattern matching fails, we can use a try-catch to handle errors, which you can use to return a friendly error that can be handled later instead of crashing the app with exception errors (although, in most Erlang cases, we let it crash).
-spec publish_xteam_article(Id :: binary()) -> ok | {error, {Error :: term(), Reason :: term()}}.
publish_xteam_article(Id) ->
try
{ok, Post} = get_xteam_article(Id),
ok = publish_xteam_article(Post)
catch
Error:Reason:StackTrace ->
%% You can optionally log the stacktrace error here if you want.
{error, {Error, Reason}}
end.
In the above example, if either get_xteam_article/1
or publish_xteam_article/1
don't return the expected results, it will move to the catch
and create a tuple error based on the reason of the crash.
2. Bit Manipulation
Binary in Erlang is magic. I like using binary to represent text strings, since it's safer to encode text. But there's more to binary in Erlang. Here's an example of Erlang easily converting a hexadecimal color to its RGB value with binary (all binary in Erlang is enclosed by <<
and >>
).
1> Color = 16#8a8a8a. %% Declaring an hexadecimal in Erlang
9079434
2> Pixel = <<Color:24>>. %% Put the hex into 24 bits
<<138,138,138>> %% Resulting <<R,G,B>> value (8 bits for each; 0 -> 255)
We can unpack the values of each color (R, G, B) and assign them to variables with pattern matching:
3> <<R:8, G:8, B:8>> = Pixel.
<<138,138,138>>
4> R.
138
5> G.
138
6> B.
138
7> <<R:8, _/binary>> = Pixel. %% you can only assign R and ignore the other elements
<<138,138,138>>
If you'd like a real-life example, as suggested in Learn You Some Erlang, you could easily parse TCP segments like this:
<<SourcePort:16, DestinationPort:16, AckNumber:32, DataOffset:4, _Reserved:4, Flags:8, WindowSize:16, CheckSum: 16, UrgentPointer:16, Payload/binary>> = RawData.
This can be extended to other protocols that use both Erlang's binary and pattern matching. Use it to identify headers based on how many bits there are and ignore anything that doesn't match your protocol.
3. Fun with Fun
In Erlang, function (fun
) is a data type. Does that mean you can assign a function to a variable? Yes, you can! Many OTP libraries actually take functions as inputs. For instance, take this lists module. The lists:filtermap/2
function lets you pass down a function that will be used to select what elements of a list will be kept and to modify the content.
Let's say we're running one process per customer, with a list of tuples containing the result of a process (ok
or error
) and an account ID. We have to notify the customer about the error, but only when they're active. We may also need a list of customers for later use, so let's use filtermap
:
1> Results = [{1, ok}, {2, error}, {3, ok}, {4, error}, {5, ok}, {6, error}].
2> GetCustomersFun = fun({Customer, error}) ->
case should_notify(Customer) of
true -> {true, Customer};
_ -> false
end
(_) -> false
end.
3> NotifyCustomers = lists:filtermap(GetCustomersFun, Results).
[4, 6]. %% should_notify/2 returned false for 2
Functions can also be used as filters (as long as they return Boolean values). See the example below that replaces list:filtermap/2
with a simple, one-list list comprehension:
4> [Customer || {Customer, error} <- Results, should_notify(Customer)].
[4, 6]
Another way I frequently use Erlang functions is when I need to call a function many times, but don't want to reload the context every time. The fun
can be built with all the required context inside:
process_users(CustomerId) ->
CustomerContext = get_context(CustomerId), %% expensive call that I only want to do one time
ProcessFun = fun(UserId) -> process(CustomerContext, UserId) end,
UserIds = get_users(CustomerContext, CustomerId),
process_users(ProcessFun, UserIds, []).
process_users(_ProcessFun, [], Results) ->
Results;
process_users(ProcessFun, [UserId|UserIds], Results0) ->
UserResult = ProcessFun(UserId),
process_users(ProcessFun, UserIds, [UserResult|Results]).
4. Processes over Threads
In Erlang, we don't deal with processor threads directly. Instead, we have an abstraction called workers
, which are lightweight processes running in the Erlang VM. Erlang is designed for concurrency, which is why Erlang's processes:
- are lightweight
- have a small memory footprint
- grow and shrink dynamically
- can be identified easily with process IDs
- can be created and terminated easily
You create a process with the spawn()
function:
1> ProcessFun = fun() -> does_something end.
#Fun<erl_eval.20.128620087>
2> Pid = spawn(ProcessFun).
<0.88.0>
The spawn()
function allows ProcessFun
in the above example to run concurrently. We can now use the process ID (PID) to look out for it or even terminate it with exit/2
:
3> exit(Pid, terminate).
true
We can also give this process a name if we need to retrieve it programmatically:
3> register(customer_1_worker, Pid).
true
4> whereis(customer_1_worker).
<0.86.0>
But Erlang goes beyond this functionality. It's designed with supervisor trees that can monitor or link processes, send messages to processes with the !
operator, and extend the capabilities of processes that use gen_server
(more on that here).
Below is an example of a fun
turned into a monitored process. The fun
uses the receive
feature that will keep listening to messages and either print out a message when it contains xteam
or kill itself:
1> Fun = fun() ->
1> receive
1> xteam -> io:format("Go check X-Team.com!~n", []);
1> _ -> exit(boom)
1> end
1> end.
#Fun<erl_eval.20.128620087>
%% spawn_monitor/1 returns a tuple of Pid and Ref, being the latter the reference of the monitor. Starting a monitor from the shell will tie the process to the shell itself (the Erlang shell is a process too)
2> {Pid, Ref} = spawn_monitor(Fun).
{<0.101.0>,#Ref<0.310719649.305135617.161147>}
3> Pid ! xteam. %% sends xteam message to Pid
Go check X-Team.com! %% since we are monitoring it in the shell, we get the message from the process back to us.
xteam
4> Pid ! exit. %% sends an exit message to Pid. The process should be terminated and a message {`EXIT`, }
exit
5> erlang:demonitor(Ref, [flush, info]). %% stops monitoring the process. This call returns false since the process is already dead and info option was provided. This also uses the flush option, which makes it flush any queued message (clearing memory)
false
5. What about IF?
If you're a C, C++, or Java developer, you might have noticed that if
clauses aren't very common in Erlang. Erlang does have an if
, but it looks different from most other languages:
crosswalk(StopLight) ->
if
StopLight =:= red -> stop();
StopLight =:= yellow -> run();
true -> walk() %% true in Erlang's `if' works as a `else' statement
end.
The if
in the above example has the entire if/elseif/if
structure built in. They must also be guard expressions (it doesn't pattern match). Because Erlang is built for pattern matching, developers usually opt to use either functions or case
statements instead:
crosswalk(red) -> stop();
crosswalk(yellow) -> run();
crosswalk(_) -> walk().
crosswalk(StopLight) ->
case StopLight of
red -> stop();
yellow -> run();
_ -> walk()
end.
If you code in Erlang, the examples above will feel more comfortable over using the language's if
. There's nothing wrong with it, but it offers less flexibility than case
or a function header. For example, when you use case
, you can go beyond pattern-matching an expression and include guards too:
crosswalk(StopLight, Daylight) ->
case StopLight of
red -> stop();
yellow when Daylight =:= false -> run();
yellow -> stop();
_ -> walk()
end.
In Conclusion
This was a (not-so-brief-after-all) primer on Erlang. I hope you learned something and I hope to have inspired you to check out this interesting, unconventional language. If you'd like to know more about Erlang, check out the following resources:
Alternatively, check out this blog post my colleague has written about Elixir, a language that branched out of Erlang.