Getting started
Hello world
-module(hello).
-export([world/0]).
world() ->
io:format("Hello, World!~n").
Save as hello.erl, compile and run:
$ erlc hello.erl # Compile to hello.beam
$ erl -noshell -s hello world -s init stop
Hello, World!
REPL basics
$ erl # Start Erlang shell
1> 2 + 3. % Expression (note the dot)
5
2> f(). % Forget all bindings
ok
3> halt(). % Exit shell
Variables
X = 10. % Assignment (once only)
Y = X + 5. % Use in expression
_Ignore = expensive(). % _ prefix for unused
Variables are immutable - once bound, cannot change.
Basic types
Numbers
42 % Integer
3.14 % Float
2#101 % Binary (5)
16#FF % Hex (255)
$A % Character code (65)
1_000_000 % Underscores for readability
Atoms
ok % Simple atom
error % Another atom
'Atom with spaces' % Quoted atom
true % Boolean atom
false % Boolean atom
undefined % Common atom
Atoms start lowercase or are quoted. Used for constants.
Strings and binaries
"Hello" % String (list of integers)
<<"Binary">> % Binary (efficient storage)
[$H, $e, $l, $l, $o] % String as list
Strings are lists: "ab" == [97, 98]
Data structures
Tuples
{ok, Value} % Common pattern
{error, Reason} % Error tuple
{Name, Age, City} % Fixed-size collection
Lists
[1, 2, 3] % List literal
[H|T] % Head and tail pattern
[] % Empty list
[1, 2 | [3, 4]] % Cons operator
Maps
#{key => value} % Create map
#{name => "Alice", age => 30}
M = #{a => 1}.
#{a := V} = M. % Match existing key (V = 1)
M2 = M#{a := 2}. % Update existing key
M3 = M#{b => 3}. % Add new key
Records
-record(person, {name, age, city}).
P = #person{name="Bob", age=25}.
P#person.name % Access field -> "Bob"
P2 = P#person{age=26}. % Update field
Records are syntactic sugar over tuples.
Pattern matching
Basic patterns
% Destructuring
{ok, Result} = {ok, 42}.
[First, Second | Rest] = [1, 2, 3, 4].
#{name := Name} = #{name => "Alice", age => 30}.
% Matching in function heads
greet({person, Name}) ->
io:format("Hello, ~s~n", [Name]).
Pattern matching in case
case Expression of
{ok, Value} ->
do_something(Value);
{error, Reason} ->
handle_error(Reason);
_ ->
default_case()
end.
Guards
max(X, Y) when X > Y -> X;
max(X, Y) -> Y.
is_valid(X) when is_integer(X), X > 0 ->
true;
is_valid(_) ->
false.
Guards: is_atom/1, is_integer/1, is_list/1, is_tuple/1, is_binary/1, is_map/1
Functions
Function clauses
-module(math).
-export([factorial/1]).
factorial(0) -> 1;
factorial(N) when N > 0 ->
N * factorial(N - 1).
Clauses separated by ;, last ends with .
Anonymous functions
Fun = fun(X) -> X * 2 end.
Fun(5). % -> 10
% Multi-clause
Abs = fun
(X) when X >= 0 -> X;
(X) -> -X
end.
Higher-order functions
lists:map(fun(X) -> X * 2 end, [1, 2, 3]).
% -> [2, 4, 6]
lists:filter(fun(X) -> X > 2 end, [1, 2, 3, 4]).
% -> [3, 4]
lists:foldl(fun(X, Acc) -> X + Acc end, 0, [1, 2, 3]).
% -> 6
List operations
Common functions
lists:reverse([1, 2, 3]) % [3, 2, 1]
lists:sort([3, 1, 2]) % [1, 2, 3]
lists:append([1, 2], [3]) % [1, 2, 3]
lists:flatten([[1], [2]]) % [1, 2]
List comprehensions
[X * 2 || X <- [1, 2, 3]].
% -> [2, 4, 6]
[X || X <- [1, 2, 3, 4], X rem 2 == 0].
% -> [2, 4]
[{X, Y} || X <- [1, 2], Y <- [a, b]].
% -> [{1,a}, {1,b}, {2,a}, {2,b}]
Membership and search
lists:member(2, [1, 2, 3]) % true
lists:keyfind(key, 1, [{key, val}]) % {key, val}
lists:any(fun(X) -> X > 5 end, [1, 2, 3]) % false
lists:all(fun(X) -> X > 0 end, [1, 2, 3]) % true
Processes
Creating processes
% Spawn a process
Pid = spawn(fun() ->
io:format("In new process~n")
end).
% Spawn with module function
Pid = spawn(module, function, [Args]).
% Spawn and link (die together)
Pid = spawn_link(Fun).
Message passing
% Send message
Pid ! {message, Data}.
% Receive message
receive
{message, Data} ->
process_data(Data);
{other, Info} ->
handle_other(Info)
after
5000 -> % Timeout in milliseconds
timeout_handler()
end.
Process info
self() % Current process PID
is_process_alive(Pid) % Check if alive
process_info(Pid) % Process information
registered() % List registered names
whereis(Name) % Get PID of registered process
Process patterns
Server loop
-module(counter).
-export([start/0, increment/1, get/1]).
start() ->
spawn(fun() -> loop(0) end).
loop(Count) ->
receive
{increment, From} ->
From ! {ok, Count + 1},
loop(Count + 1);
{get, From} ->
From ! {value, Count},
loop(Count)
end.
increment(Pid) ->
Pid ! {increment, self()},
receive {ok, NewCount} -> NewCount end.
get(Pid) ->
Pid ! {get, self()},
receive {value, Count} -> Count end.
Process registration
% Register process with name
register(server_name, Pid).
% Send to registered process
server_name ! Message.
% Unregister
unregister(server_name).
Error handling
Try-catch
try
risky_operation()
catch
error:Reason ->
{error, Reason};
throw:Term ->
{thrown, Term};
exit:Reason ->
{exit, Reason}
after
cleanup() % Always executed
end.
Raising errors
error(Reason) % Error exception
throw(Term) % Throw exception
exit(Reason) % Exit current process
erlang:raise(Class, Reason, Stacktrace) % Re-raise
Process links and monitors
% Link (bidirectional)
link(Pid).
unlink(Pid).
% Monitor (unidirectional)
Ref = monitor(process, Pid).
demonitor(Ref).
% Trap exits
process_flag(trap_exit, true).
receive
{'EXIT', Pid, Reason} ->
handle_exit(Reason)
end.
Module structure
Module declaration
-module(mymodule). % Module name (matches filename)
-export([func1/1, func2/2]). % Public functions
-import(lists, [map/2, filter/2]). % Import functions
-define(TIMEOUT, 5000). % Macro definition
-record(state, {count, name}). % Record definition
% Private function (not exported)
helper(X) -> X + 1.
Compilation
$ erlc mymodule.erl # Compile to .beam
$ erlc +debug_info mymodule.erl # With debug info
$ erlc -o ebin src/mymodule.erl # Output directory
In Erlang shell:
c(mymodule). % Compile and load
l(mymodule). % Reload module
m(). % List loaded modules
OTP basics
gen_server behavior
-module(my_server).
-behaviour(gen_server).
-export([start_link/0]).
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
{ok, #{count => 0}}.
handle_call(get_count, _From, State = #{count := Count}) ->
{reply, Count, State};
handle_call({increment, N}, _From, State = #{count := Count}) ->
{reply, ok, State#{count := Count + N}}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
Using gen_server
{ok, Pid} = my_server:start_link().
gen_server:call(Pid, get_count). % Synchronous
gen_server:cast(Pid, async_message). % Asynchronous
Common modules
io module
io:format("Hello ~s~n", ["World"]) % Print formatted
io:get_line("Prompt: ") % Read line
io:read("Enter: ") % Read term
io:fwrite("Text~n") % Write output
Format specifiers: ~s (string), ~p (pretty), ~w (write), ~n (newline)
lists module
lists:map(Fun, List)
lists:filter(Fun, List)
lists:foldl(Fun, Acc, List)
lists:foldr(Fun, Acc, List)
lists:foreach(Fun, List)
lists:reverse(List)
lists:sort(List)
lists:zip(List1, List2)
lists:unzip(ListOfTuples)
string module
string:length("hello") % 5
string:concat("Hello", " World") % "Hello World"
string:substr("hello", 2, 3) % "ell"
string:tokens("a,b,c", ",") % ["a", "b", "c"]
string:uppercase("hello") % "HELLO"
string:lowercase("HELLO") % "hello"
file module
file:read_file("file.txt") % {ok, Binary}
file:write_file("file.txt", Data) % ok | {error, Reason}
file:consult("config.erl") % Read Erlang terms
file:list_dir(".") % List directory
file:delete("file.txt") % Delete file
timer module
timer:sleep(1000) % Sleep 1 second
timer:send_after(1000, Pid, Msg) % Send after delay
timer:send_interval(1000, Pid, Msg) % Send repeatedly
timer:apply_after(1000, M, F, A) % Apply after delay
calendar module
calendar:local_time() % {{Y,M,D}, {H,M,S}}
calendar:universal_time() % UTC time
calendar:now_to_local_time(Now) % Convert timestamp
Binary operations
Binary construction
<<1, 2, 3>> % 3 bytes
<<X:16>> % 16-bit integer
<<X:32/float>> % 32-bit float
<<X:8/binary>> % 8-byte binary
<<"hello">> % Binary string
<<1:1, 0:1, 1:1>> % Bits (not bytes)
Binary pattern matching
<<Head:8, Rest/binary>> = <<1, 2, 3>>.
% Head = 1, Rest = <<2, 3>>
<<A:4, B:4>> = <<255>>.
% A = 15, B = 15
<<"GET ", Rest/binary>> = <<"GET /index.html">>.
% Rest = <<"/index.html">>
Macros and includes
Macros
-define(TIMEOUT, 5000).
-define(MAX(X, Y),
if X > Y -> X; true -> Y end).
% Usage
timer:sleep(?TIMEOUT).
Result = ?MAX(10, 20).
Include files
-include("header.hrl"). % Include file
-include_lib("stdlib/include/ms_transform.hrl").
In header.hrl:
-define(CONST, 42).
-record(rec, {field1, field2}).
Gotchas
Variable binding
% ❌ Variables cannot be rebound
X = 1.
X = 2. % Error: no match
% ✅ Use new variable
X = 1.
Y = 2.
% ✅ Use f() to forget in shell
X = 1.
f(X). % Forget X binding
X = 2. % Now OK
String vs binary
% String is list of integers
"hello" == [104, 101, 108, 108, 111] % true
% ⚠️ Strings are inefficient for large data
Data = "large text...". % List (slow)
Data = <<"large text...">>. % Binary (fast)
Message ordering
% ⚠️ Messages are ordered per sender
% But NOT across different senders
Pid ! msg1.
Pid ! msg2. % msg2 arrives after msg1
% ⚠️ Selective receive can reorder
receive
pattern2 -> ok % May receive pattern2 first
after 0 ->
receive pattern1 -> ok end
end.
Process dictionary
% ⚠️ Avoid process dictionary (global mutable state)
put(key, value). % Sets in process dict
get(key). % Gets from process dict
% ✅ Use process state or ETS instead
List append performance
% ❌ Inefficient: O(n) per append
List1 = [1, 2, 3].
List2 = List1 ++ [4]. % Copies entire List1
% ✅ Prepend is O(1)
List2 = [4 | List1]. % Fast
lists:reverse([4 | List1]). % If order matters
Shell commands
Compilation
c(module). % Compile module
c(module, [debug_info]). % With debug info
l(module). % Load/reload
Information
m(). % List modules
m(module). % Module info
h(module, function). % Help
pid(0, 250, 0). % Create PID
Process management
i(). % Process information
regs(). % Registered processes
processes(). % All PIDs
exit(Pid, kill). % Kill process
Memory and debugging
memory(). % Memory usage
erlang:system_info(process_count). % Process count
dbg:tracer(). % Start tracer
dbg:p(all, c). % Trace all calls
Testing with EUnit
Basic test
-module(math_test).
-include_lib("eunit/include/eunit.hrl").
simple_test() ->
?assertEqual(4, 2 + 2).
list_test() ->
?assert(length([1, 2, 3]) == 3).
Test fixtures
setup_test_() ->
{setup,
fun() -> setup_code() end, % Setup
fun(Ctx) -> cleanup(Ctx) end, % Cleanup
fun(Ctx) -> test_body(Ctx) end % Test
}.
Run tests:
$ erlc -o ebin src/*.erl test/*.erl
$ erl -noshell -pa ebin -eval "eunit:test(math_test, [verbose])" -s init stop
Supervisor trees
Basic supervisor
-module(my_supervisor).
-behaviour(supervisor).
-export([start_link/0, init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
SupFlags = #{
strategy => one_for_one,
intensity => 10,
period => 60
},
ChildSpecs = [
#{
id => worker1,
start => {worker, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [worker]
}
],
{ok, {SupFlags, ChildSpecs}}.
Restart strategies
% one_for_one: Restart only failed child
strategy => one_for_one
% one_for_all: Restart all children if one fails
strategy => one_for_all
% rest_for_one: Restart failed child + those started after it
strategy => rest_for_one
% simple_one_for_one: For dynamic children
strategy => simple_one_for_one
SupFlags map
#{
strategy => one_for_one, % Restart strategy
intensity => 10, % Max restarts
period => 60 % In seconds
}
If intensity restarts happen within period seconds, supervisor terminates.
ChildSpec map
#{
id => unique_id, % Child identifier
start => {M, F, A}, % Start function
restart => permanent, % permanent | transient | temporary
shutdown => 5000, % Timeout (ms) or brutal_kill
type => worker, % worker | supervisor
modules => [module] % For hot code upgrades
}
Restart options
restart => permanent % Always restart
restart => transient % Restart if abnormal exit
restart => temporary % Never restart
Dynamic children
% For simple_one_for_one supervisors
supervisor:start_child(SupPid, [Args]).
supervisor:terminate_child(SupPid, ChildPid).
supervisor:which_children(SupPid).
supervisor:count_children(SupPid).
gen_statem
State Functions mode
-module(door).
-behaviour(gen_statem).
-export([start_link/0, open/0, close/0]).
-export([init/1, callback_mode/0, terminate/3]).
-export([locked/3, unlocked/3]).
start_link() ->
gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
{ok, locked, #{}}.
callback_mode() ->
state_functions.
% State: locked
locked({call, From}, unlock, Data) ->
{next_state, unlocked, Data, [{reply, From, ok}]};
locked(EventType, EventContent, Data) ->
{keep_state, Data}.
% State: unlocked
unlocked({call, From}, lock, Data) ->
{next_state, locked, Data, [{reply, From, ok}]};
unlocked({call, From}, open, Data) ->
{keep_state, Data, [{reply, From, ok}]}.
terminate(_Reason, _State, _Data) ->
ok.
Handle Event Function mode
-module(door2).
-behaviour(gen_statem).
-export([init/1, callback_mode/0, handle_event/4]).
init([]) ->
{ok, locked, #{}}.
callback_mode() ->
handle_event_function.
handle_event({call, From}, unlock, locked, Data) ->
{next_state, unlocked, Data, [{reply, From, ok}]};
handle_event({call, From}, lock, unlocked, Data) ->
{next_state, locked, Data, [{reply, From, ok}]};
handle_event(EventType, EventContent, State, Data) ->
{keep_state, Data}.
Return values
% Change state
{next_state, NewState, NewData}
{next_state, NewState, NewData, Actions}
% Keep current state
{keep_state, NewData}
{keep_state, NewData, Actions}
% Keep state and data
keep_state_and_data
{keep_state_and_data, Actions}
Actions and timeouts
% State timeout (reset on state change)
{state_timeout, 5000, timeout_event}
% Generic timeout
{timeout, 5000, timeout_event}
% Event timeout (reset on any event)
{{timeout, Name}, 5000, timeout_event}
% Reply action
{reply, From, Reply}
Client API
gen_statem:call(Name, Request).
gen_statem:cast(Name, Event).
gen_statem:stop(Name).
Application behaviour
Application callback module
-module(myapp_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
myapp_supervisor:start_link().
stop(_State) ->
ok.
.app resource file
% src/myapp.app.src
{application, myapp, [
{description, "My Application"},
{vsn, "1.0.0"},
{registered, [myapp_sup]},
{mod, {myapp_app, []}},
{applications, [
kernel,
stdlib,
sasl
]},
{env, [
{port, 8080},
{host, "localhost"}
]},
{modules, []},
{licenses, ["Apache-2.0"]},
{links, []}
]}.
Application control
application:start(myapp).
application:stop(myapp).
application:which_applications().
application:loaded_applications().
% Get environment variable
application:get_env(myapp, port). % {ok, 8080}
application:get_env(myapp, port, 3000). % 8080 or default
Start types
start(normal, _Args) ->
% Normal start
myapp_supervisor:start_link();
start({takeover, Node}, _Args) ->
% Taking over from Node
myapp_supervisor:start_link();
start({failover, Node}, _Args) ->
% Failover from Node
myapp_supervisor:start_link().
Distributed Erlang
Starting nodes
# Start with short name (local network)
$ erl -sname node1
# Start with full name (distributed)
$ erl -name node1@hostname.domain
# Set cookie (for authentication)
$ erl -sname node1 -setcookie secret
Node operations
node(). % Current node name
nodes(). % Connected nodes
nodes(connected). % Same as nodes()
nodes(hidden). % Hidden nodes
% Connect to node
net_adm:ping('node2@hostname'). % pong | pang
% Spawn on remote node
spawn('node2@hostname', fun() -> code() end).
spawn('node2@hostname', Module, Function, Args).
Magic cookies
% Set cookie at runtime
erlang:set_cookie(node(), secret).
erlang:set_cookie('node2@hostname', other_secret).
% Get cookie
erlang:get_cookie().
Nodes with different cookies cannot communicate.
Monitoring nodes
% Monitor node connection
monitor_node('node2@hostname', true).
receive
{nodedown, Node} ->
io:format("Node ~p went down~n", [Node])
end.
% Stop monitoring
monitor_node('node2@hostname', false).
Global name registration
% Register name globally
global:register_name(myserver, Pid).
% Send to global name
global:send(myserver, Message).
% Lookup global name
global:whereis_name(myserver). % Pid | undefined
ETS (Erlang Term Storage)
Creating tables
% Create table
ets:new(tablename, [set, named_table, public]).
% Table types
ets:new(t, [set]). % Key-value (unique keys)
ets:new(t, [ordered_set]). % Ordered keys
ets:new(t, [bag]). % Duplicate keys, unique values
ets:new(t, [duplicate_bag]). % Duplicate keys and values
Access control
% Access types
[public] % All processes can read/write
[protected] % All read, owner writes (default)
[private] % Only owner can access
% Named tables
[named_table] % Access by name, not Tid
Basic operations
% Insert
ets:insert(tablename, {key, value}).
ets:insert(tablename, [{key1, val1}, {key2, val2}]).
% Lookup
ets:lookup(tablename, key). % [{key, value}] or []
ets:lookup_element(tablename, key, 2). % value (crashes if not found)
% Delete
ets:delete(tablename, key). % Delete entry
ets:delete(tablename). % Delete table
ets:delete_all_objects(tablename). % Empty table
Match operations
% Match pattern
ets:match(tablename, {key, '$1'}). % [[value1], [value2], ...]
ets:match(tablename, {'$1', value}). % [[key1], [key2], ...]
% Match object (return full tuples)
ets:match_object(tablename, {key, '_'}). % [{key, val1}, {key, val2}]
% Select with match spec
ets:select(tablename, [{
{key, '$1'}, % Pattern
[], % Guards
['$1'] % Result
}]).
Table information
ets:info(tablename). % All info
ets:info(tablename, size). % Number of entries
ets:info(tablename, memory). % Memory in words
ets:info(tablename, owner). % Owner PID
ets:all(). % All table IDs
ets:tab2list(tablename). % Convert to list
Iteration
% First/next iteration
Key = ets:first(tablename).
NextKey = ets:next(tablename, Key).
% Fold
ets:foldl(fun({K, V}, Acc) ->
[{K, V} | Acc]
end, [], tablename).
Type specifications
Function specs
-spec function_name(Type1, Type2) -> ReturnType.
function_name(Arg1, Arg2) ->
...
% Examples
-spec add(integer(), integer()) -> integer().
add(X, Y) -> X + Y.
-spec divide(number(), number()) -> {ok, float()} | {error, atom()}.
divide(_X, 0) -> {error, division_by_zero};
divide(X, Y) -> {ok, X / Y}.
Type definitions
-type name() :: atom().
-type age() :: non_neg_integer().
-type person() :: {name(), age()}.
-type result(T) :: {ok, T} | {error, term()}.
% Opaque type (implementation hidden)
-opaque private_data() :: {internal, term()}.
% Export types
-export_type([person/0, result/1]).
Common built-in types
% Basic types
any() % Any type
none() % No value
atom()
integer()
float()
number() % integer() | float()
binary()
bitstring()
boolean() % true | false
% Compound types
list()
list(T) % List of T
tuple()
map()
% Special types
pid()
reference()
fun()
fun((T1, T2) -> T3) % Function type
Advanced type patterns
% Union types
-type status() :: running | stopped | paused.
-type id() :: integer() | binary().
% Record types
-record(user, {name :: string(), age :: integer()}).
-type user() :: #user{}.
% Constrained types
-spec get_user(Id) -> {ok, user()} | {error, not_found}
when Id :: pos_integer().
Running Dialyzer
# Build PLT (Persistent Lookup Table)
$ dialyzer --build_plt --apps erts kernel stdlib
# Analyze application
$ dialyzer ebin/
# With options
$ dialyzer -r src/ --src
$ dialyzer --no_check_plt ebin/
In Erlang shell:
dialyzer:run([{files, ["ebin/mymodule.beam"]}]).
rebar3 basics
Project creation
# Create application
$ rebar3 new app myapp
# Create library
$ rebar3 new lib mylib
# Create release
$ rebar3 new release myrelease
# Create Escript
$ rebar3 new escript myscript
Common commands
# Compile
$ rebar3 compile
# Run tests
$ rebar3 eunit
$ rebar3 ct # Common Test
$ rebar3 do eunit, ct # Run both
# Start shell
$ rebar3 shell
# Create release
$ rebar3 release
# Clean
$ rebar3 clean
rebar.config
{erl_opts, [
debug_info,
warnings_as_errors,
{i, "include"}
]}.
{deps, [
{cowboy, "2.10.0"},
{jsx, "3.1.0"},
{mochiweb, {git, "https://github.com/mochi/mochiweb.git", {tag, "v2.22.0"}}}
]}.
{plugins, [
rebar3_hex
]}.
{profiles, [
{prod, [
{erl_opts, [no_debug_info, warnings_as_errors]},
{relx, [{dev_mode, false}]}
]},
{test, [
{deps, [{meck, "0.9.2"}]}
]}
]}.
Dependency management
# Get dependencies
$ rebar3 get-deps
# Upgrade dependencies
$ rebar3 upgrade
# Lock dependencies
$ rebar3 lock
# List dependencies
$ rebar3 tree
Release commands
# Create release
$ rebar3 release
# Run release
$ _build/default/rel/myapp/bin/myapp console
# Create tarball
$ rebar3 tar
External resources
Storage reference
- ETS (Erlang Term Storage) - See section above for in-memory table operations
Also see
- Learn You Some Erlang - Comprehensive free book
- Erlang.org Documentation - Official documentation
- Erlang Reference Manual - Language reference
- OTP Design Principles - OTP patterns and behaviors
- Erlang Standard Library - STDLIB reference