πŸ“‘ 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.

πŸ“Œ BEAM VMπŸ“Œ Actor ModelπŸ“Œ Let it CrashπŸ“Œ Preemptive Scheduling
%% 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 Operator⚑ Wildcard (_)πŸ“Š Binding vs MatchingπŸ“ Destructuring
%% 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).

🎨 Guards (when)πŸ“¦ Tail RecursionπŸ’Ύ AccumulatorsπŸ” Higher-Order Functions
%% 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.

πŸš€ Cons Operator (|)πŸ“Š List ComprehensionsπŸ“ lists:foldl/3πŸ” Lazy Streams
%% 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.

🧬 spawn/3πŸ’Ύ Process DictionaryπŸ“¦ Registered ProcessesπŸ” monitor/2
%% 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.

βœ‰οΈ receive Block⏱️ after TimeoutπŸ“¬ Selective ReceiveπŸ” flush/0
%% 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.

πŸ›‘οΈ Supervisor BehavioursπŸ“Š Restart Strategies⚑ Maximum Restart IntensityπŸ” Worker vs Supervisor
%% 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 Callbacksβš™οΈ gen_statemπŸ“ sys ModuleπŸ”§ OTP Applications
%% 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.

πŸ“ ets:new/2⚑ ets:insert/2πŸ“Š ets:lookup/2πŸ” Match Specifications
%% 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 SyntaxπŸ” Map UpdatesπŸ“ Pattern Matching⚑ Maps:get/3
%% 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/after⚑ throw/1πŸ“Š exit/1πŸ” error/1
%% 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 Syntax⚑ Zero-CopyπŸ“Š Bit StringsπŸ” Size/Type Specifiers
%% 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.

πŸ”Œ open_port/2⚑ NIFsπŸ“Š erl_interfaceπŸ” Port Control
%% 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).

🌐 Node NamesπŸ“‘ net_admπŸ” global ModuleπŸ” Cookie Auth
%% 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