π‘ Erlang Deep Foundations
π― Erlang Design Philosophy & Execution Model
Erlang (Joe Armstrong, 1986, Ericsson) is a functional, concurrent, garbage-collected language designed for massive scalability and fault tolerance. It runs on the BEAM (Bogdan/BjΓΆrn's Erlang Abstract Machine) VM. Key principles: "Let it crash", actor model concurrency, and immutability. Originally built for telecom switches requiring 99.9999999% (nine nines) reliability.
Deep Explanation
Erlang processes are isolated and lightweight: Each process has its own heap and GC β millions can run simultaneously. The BEAM VM uses preemptive scheduling, meaning no process can monopolize the CPU. Variables are single-assignment (bind once, never mutate).
Why it matters: Erlang powers WhatsApp (2M+ concurrent connections), RabbitMQ, CouchDB, and telecom infrastructure. Its fault-tolerant design means if one process crashes, others continue unaffected.
%% Erlang Deep Foundations
%% Run in Erlang shell (erl)
%% Single-assignment variables (starts with capital)
X = 42.
%% X = 43. % ERROR! Variable already bound
%% Basic data types
Integer = 100.
Float = 3.14159.
Atom = hello. % atoms: lowercase constants
Boolean = true. % atoms true/false
String = "hello". % string is list of chars
List = [1,2,3,4].
Tuple = {ok, "success"}.
%% Shell commands
% q(). % quit shell
% help(). % get help
% i(). % process infoπ Immutability & Pattern Matching
π― Single Assignment & Destructuring
Variables are bound once (immutable). Pattern matching is used for assignment, control flow, and data extraction β not just a feature but the core mechanism.
Pattern Matching Everywhere
In Erlang, = is the pattern matching operator (not assignment). It succeeds if the left pattern matches the right expression. The _ wildcard matches anything. Pattern matching appears in function heads, case statements, receive blocks, and try/catch.
%% Pattern matching basics
{Hours, Minutes, Seconds} = {10, 30, 45}.
Hours. % 10
Minutes. % 30
%% List destructuring
[First | Rest] = [1,2,3,4,5].
First. % 1
Rest. % [2,3,4,5]
%% Ignoring values with _
{ok, Data, _Rest} = {ok, "hello", "world"}.
%% Pattern matching in function heads
factorial(0) -> 1;
factorial(N) -> N * factorial(N-1).
%% Case expression
case {ok, "result"} of
{ok, Result} -> io:format("Got: ~p~n", [Result]);
{error, Reason} -> io:format("Error: ~p~n", [Reason])
end.π― Functions & Recursion Mastery
π― Functions, Guards, Higher-Order
Functions are first-class. Guards (when) add pattern matching conditions. Recursion replaces loops β with tail-call optimization guaranteed.
Tail Recursion & Accumulators
Erlang compilers optimize tail-recursive functions into loops, preventing stack overflow. Accumulators (extra parameters) carry partial results. Functions can be exported from modules and used as arguments (lists:map/2, lists:foldl/3).
%% Module definition
-module(math_utils).
-export([factorial/1, fibonacci/1, sum/1, map/2]).
%% Recursive factorial (tail-optimized)
factorial(N) -> factorial(N, 1).
factorial(0, Acc) -> Acc;
factorial(N, Acc) -> factorial(N-1, N * Acc).
%% Fibonacci with multiple clauses
fibonacci(0) -> 0;
fibonacci(1) -> 1;
fibonacci(N) when N > 1 -> fibonacci(N-1) + fibonacci(N-2).
%% List sum with tail recursion
sum(List) -> sum(List, 0).
sum([], Acc) -> Acc;
sum([H|T], Acc) -> sum(T, Acc + H).
%% Higher-order function
map([], _Func) -> [];
map([H|T], Func) -> [Func(H) | map(T, Func)].
%% Anonymous functions (funs)
Double = fun(X) -> X * 2 end.
map([1,2,3], Double). % [2,4,6]π List Comprehensions & Recursion
π― List Comprehensions & List Module
Lists are the primary data structure. List comprehensions provide declarative transformations: [Expression || Generator, Filter...].
Efficient List Operations
The lists module provides map/2, filter/2, foldl/3, sort/1, etc. List comprehensions are syntactic sugar for common patterns. Cons operator (|) prepends O(1); appending (++) is O(n) β build lists from head.
%% List comprehensions
%% Squares of evens
Squares = [X*X || X <- [1,2,3,4,5,6], X rem 2 == 0].
% [4, 16, 36]
%% Cartesian product
Pairs = [{X,Y} || X <- [1,2], Y <- [a,b,c]].
% [{1,a},{1,b},{1,c},{2,a},{2,b},{2,c}]
%% Generating sequences
Evens = [X || X <- lists:seq(1, 20), X rem 2 == 0].
%% Using lists module
List = [5,2,8,1,9].
Sorted = lists:sort(List).
Reversed = lists:reverse(List).
Sum = lists:foldl(fun(X, Acc) -> X + Acc end, 0, List).
%% Building list efficiently (from head)
build_list(N) -> build_list(N, []).
build_list(0, Acc) -> Acc;
build_list(N, Acc) -> build_list(N-1, [N | Acc]).π Lightweight Processes (Actors)
π― Millions of Isolated Processes
Erlang processes are not OS threads β they're lightweight VM-managed actors with ~300 words overhead. Created with spawn/1,2,3,4.
Process Lifecycle & Scheduling
Each process has a unique PID. spawn(fun() -> ... end) creates a process. self() returns current PID. The BEAM scheduler distributes processes across CPU cores. Processes share no memory β only communicate via messages.
%% Creating processes
Pid = spawn(fun() -> loop() end).
%% Process with registered name
register(my_server, spawn(fun() -> server_loop() end)).
whereis(my_server). % returns Pid
%% Process with function export
spawn(module, function_name, [Arg1, Arg2]).
%% Process monitoring
Pid = spawn(fun() -> timer:sleep(1000) end).
Ref = monitor(process, Pid).
receive
{'DOWN', Ref, process, Pid, Reason} ->
io:format("Process died: ~p~n", [Reason])
end.
%% Simple counter process
counter(Value) ->
receive
increment -> counter(Value + 1);
{get, From} -> From ! {value, Value}, counter(Value)
end.βοΈ Message Passing & Mailboxes
π― Asynchronous Message Sending
Pid ! Message sends a message asynchronously. Each process has a mailbox (FIFO queue). receive extracts messages with pattern matching.
Selective Receive & Timeouts
receive can specify multiple patterns with guards. Messages not matching any pattern stay in the mailbox. Timeouts (after) prevent indefinite waiting. This selective receive is what enables complex protocol implementations.
%% Basic send and receive
Pid = spawn(fun() ->
receive
{hello, From} -> From ! "Hello back!"
end
end).
Pid ! {hello, self()}.
receive
Response -> io:format("Got: ~p~n", [Response])
end.
%% Selective receive (order matters)
receive
{priority, Msg} -> handle_priority(Msg)
after 0 -> %% check mailbox non-blocking
receive
{normal, Msg} -> handle_normal(Msg)
end
end.
%% Timeout example
wait_for_message(Timeout) ->
receive
Msg -> {ok, Msg}
after Timeout ->
timeout
end.
%% Flush all messages
flush() ->
receive
Msg -> io:format("Flushed: ~p~n", [Msg]), flush()
after 0 -> ok
end.π‘οΈ Supervision Trees & Fault Tolerance
π― "Let It Crash" Philosophy
Supervisors monitor child processes. If a child crashes, the supervisor restarts it according to a strategy (one_for_one, one_for_all, rest_for_one).
Building Reliable Systems
Don't write defensive code β write code that assumes success and crashes if expectations fail. Supervisors handle recovery. This yields simpler, more reliable code than traditional error-checking.
%% Basic supervisor (using OTP)
-module(my_supervisor).
-behaviour(supervisor).
-export([start_link/0, init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
%% Child specification
Child = #{id => worker1,
start => {my_worker, start_link, []},
restart => permanent, %% always restart
shutdown => brutal_kill, %% immediate kill
type => worker,
modules => [my_worker]},
%% Restart strategy: one_for_one, one_for_all, rest_for_one
{ok, {{one_for_one, 5, 10}, [Child]}}.
%% Link processes (manual supervision)
Pid = spawn_link(fun() -> risky_function() end).
process_flag(trap_exit, true). %% catch exit signals
receive
{'EXIT', Pid, Reason} -> io:format("Child crashed: ~p~n", [Reason])
end.βοΈ OTP Behaviours (gen_server, gen_statem)
π― Industrial-Strength Patterns
Open Telecom Platform (OTP) provides reusable behaviours: gen_server (client-server), gen_statem (state machines), gen_event (event handling), application.
OTP Makes Erlang Production-Ready
Behaviours provide standardized patterns for code reloading, logging, and debugging. gen_server handles message queues, timeouts, and call/cast semantics automatically. sys:get_status/1 inspects any OTP process.
%% gen_server implementation
-module(key_value_store).
-behaviour(gen_server).
-export([start_link/0, put/2, get/1]).
-export([init/1, handle_call/3, handle_cast/2, terminate/2]).
%% Client API
start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
put(Key, Value) -> gen_server:cast(?MODULE, {put, Key, Value}).
get(Key) -> gen_server:call(?MODULE, {get, Key}).
%% Callbacks
init([]) -> {ok, maps:new()}.
handle_call({get, Key}, _From, State) ->
{reply, maps:get(Key, State, undefined), State}.
handle_cast({put, Key, Value}, State) ->
{noreply, maps:put(Key, Value, State)}.
terminate(_Reason, _State) -> ok.
%% Usage
%% key_value_store:start_link().
%% key_value_store:put("name", "Erlang").
%% key_value_store:get("name").πΎ ETS (Erlang Term Storage)
π― In-Memory Database
ETS provides massive, in-memory tables for storing Erlang terms. Data is shared between processes (no copying). Types: set, ordered_set, bag, duplicate_bag.
ETS for Caching & State
ETS tables are faster than process dictionaries for large data. public tables are readable/writable by any process. protected tables allow all reads, only owner writes. private tables are owner-only. ETS is the basis for Mnesia database.
%% Create ETS table
Table = ets:new(my_table, [set, public, named_table]).
%% Insert data
ets:insert(my_table, {key1, "value1"}).
ets:insert(my_table, [{key2, "value2"}, {key3, "value3"}]).
%% Lookup
case ets:lookup(my_table, key1) of
[{key1, Value}] -> io:format("Found: ~p~n", [Value]);
[] -> io:format("Not found~n")
end.
%% Match (find all keys starting with 'user')
Pattern = {'$1', '$2'},
Guard = [],
Result = {'$1'},
Matches = ets:match(my_table, Pattern, 100). % limited to 100
%% Delete
ets:delete(my_table, key1).
ets:delete_all_objects(my_table).
ets:delete(my_table). % destroy table
%% ETS with bag (multiple values per key)
BagTable = ets:new(bag_table, [bag]).
ets:insert(BagTable, {user, "admin"}).
ets:insert(BagTable, {user, "guest"}).
ets:lookup(BagTable, user). % returns both valuesπ Records & Maps
π― Structured Data Types
Records are compile-time tuples with named fields. Maps are dynamic key-value stores (similar to dicts).
When to Use What
Records are ideal for fixed schemas (compile-time safety, faster access). Maps are flexible for dynamic keys. Both support pattern matching. Records are defined with -record(name, {field1, field2 = default}).
%% Record definition
-record(person, {name, age = 0, email}).
%% Creating records
Person = #person{name="Alice", age=30}.
%% Access fields
Name = Person#person.name.
%% Update (creates new record)
OlderPerson = Person#person{age = 31}.
%% Pattern matching with records
case Person of
#person{name="Alice", age=Age} -> io:format("Alice is ~p~n", [Age]);
_ -> ok
end.
%% Maps (dynamic)
Map = #{name => "Bob", age => 25, roles => [admin, user]}.
Name = maps:get(name, Map).
NewMap = Map#{age => 26}.
AddressMap = #{city => "Stockholm", zip => 11122}.
Merged = maps:merge(Map, AddressMap).
%% Map pattern matching
#{name := Name, age := Age} = Map.
%% Name = "Bob", Age = 25π Error Handling (throw, catch, try)
π― Exceptions vs Let It Crash
Erlang has three exception classes: throw (non-local return), error (runtime errors), exit (process death).
Exception Handling Patterns
try ... catch handles exceptions. throw(Value) is for non-local returns. exit(Reason) terminates the process (supervisor handles). Use error(Reason) for programmer errors. The after clause ensures cleanup.
%% Try-Catch with patterns
try
risky_function()
catch
throw:Value -> io:format("Caught throw: ~p~n", [Value]);
error:Reason -> io:format("Caught error: ~p~n", [Reason]);
exit:Reason -> io:format("Caught exit: ~p~n", [Reason])
after
cleanup()
end.
%% Using throw for non-local return
find_element(Pred, List) ->
try
do_find(Pred, List)
catch
throw:found -> found
end.
do_find(_, []) -> throw(not_found);
do_find(Pred, [H|_]) when Pred(H) -> throw(found);
do_find(Pred, [_|T]) -> do_find(Pred, T).
%% Process exit trapping
process_flag(trap_exit, true).
Pid = spawn_link(fun() -> exit(bad_thing) end).
receive
{'EXIT', Pid, Reason} -> io:format("Process died: ~p~n", [Reason])
end.π¦ Binary Pattern Matching
π― Binary Construction & Matching
Erlang has first-class support for binary data. <<...>> syntax for constructing and matching binary data β ideal for network protocols.
Parsing Protocols Efficiently
Binary pattern matching extracts fields by size, type, and endianness. Zero-copy operations make it extremely fast. Common in BitTorrent, DNS, and TCP/IP implementations. The bitstring type handles arbitrary bit lengths.
%% Binary construction
Binary = <<1,2,3,4>>.
StringBinary = <<"hello">>.
Mixed = <<1, "abc", 3.14/float>>.
%% Binary pattern matching
<<A:8, B:8, Rest/binary>> = <<1,2,3,4,5>>.
%% A = 1, B = 2, Rest = <<3,4,5>>
%% Parsing TCP packet (16-bit length, then data)
parse_packet(<<Length:16, Data:Length/binary, Rest/binary>>) ->
process_packet(Data),
parse_packet(Rest);
parse_packet(<<>>) -> ok.
%% Bit-level matching
<<Flag1:1, Flag2:1, Value:6>> = <<13>>.
%% Flag1 = 0, Flag2 = 0, Value = 13
%% Endianness
LittleEndian = <<X:16/little, Y:16/little>> = <<1,0,2,0>>.
%% X = 1, Y = 2π Ports & NIFs (C Interop)
π― Interfacing with External Code
Ports communicate with external OS processes via stdin/stdout. NIFs (Native Implemented Functions) call C code directly in the VM.
Choosing Between Ports and NIFs
Ports are safer but slower β external process crashes don't affect the VM. NIFs are faster but dangerous β a crash brings down the entire VM. Use linked-in drivers for hardware integration. The erl_interface library helps build C nodes.
%% Port to external program
Port = open_port({spawn, "cat"}, [binary, in, out, exit_status]).
port_command(Port, <<"Hello world\n">>).
receive
{Port, {data, Data}} -> io:format("Got: ~s~n", [Data]);
{Port, {exit_status, Status}} -> io:format("Exit: ~p~n", [Status])
end.
%% NIF example (C code compiled as .so)
%% -on_load(init_nif/0).
%% init_nif() -> erlang:load_nif("./my_nif", 0).
%% Erlang side of NIF
my_function(_Arg) ->
erlang:nif_error(nif_not_loaded).
%% Port driver registration
register_port_driver() ->
erl_ddll:load_driver(".", "my_driver"),
Port = open_port({spawn, "my_driver"}, []).π Distributed Erlang & Nodes
π― Transparent Distribution
Erlang nodes can communicate seamlessly across networks. Send messages to processes on remote nodes: {RemotePid, Node} ! Message.
Clustering & Global Registration
net_adm:ping/1 tests connectivity. global:register_name/2 registers names cluster-wide. pg module provides process groups. Distribution uses TCP, encrypted with SSL. Cookie-based authentication (~/.erlang.cookie).
%% Start Erlang with node name
%% $ erl -sname mynode@localhost
%% Connect to another node
net_adm:ping('othernode@hostname').
nodes(). % List connected nodes
%% Remote process spawning
RemoteNode = 'worker@server'.
Pid = spawn(RemoteNode, fun() -> loop() end).
%% Send message to remote process
Pid ! {self(), hello}.
%% Global registration
global:register_name(counter_server, Pid).
global:whereis_name(counter_server).
%% Multi-node RPC
rpc:call(RemoteNode, io, format, ["Hello from ~p~n", [node()]]).
%% Run function on all nodes
[rpc:call(Node, module, function, [Args]) || Node <- nodes()].
%% Set up hidden node (not in global namespace)
%% $ erl -sname hidden -hidden