Claude-skill-registry lang-erlang-dev
Foundational Erlang patterns covering OTP behaviors, fault-tolerant systems, distributed computing, pattern matching, processes, and supervision trees. Use when writing Erlang code, building concurrent systems, working with OTP frameworks, or developing distributed fault-tolerant applications. This is the entry point for Erlang development.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/lang-erlang-dev" ~/.claude/skills/majiayu000-claude-skill-registry-lang-erlang-dev && rm -rf "$T"
manifest:
skills/data/lang-erlang-dev/SKILL.mdsource content
Erlang Fundamentals
Foundational Erlang patterns and core language features for building fault-tolerant, distributed systems. This skill serves as both a reference for common patterns and an index to specialized Erlang skills.
Overview
┌─────────────────────────────────────────────────────────────────┐ │ Erlang Skill Hierarchy │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────┐ │ │ │ lang-erlang-dev │ ◄── You are here │ │ │ (foundation) │ │ │ └──────────┬──────────┘ │ │ │ │ │ ┌────────────────┼────────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ erlang │ │ otp │ │ distributed │ │ │ │ patterns │ │behaviors │ │ systems │ │ │ └──────────────┘ └──────────┘ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘
This skill covers:
- Pattern matching and guards
- Processes and message passing
- OTP behaviors (gen_server, gen_statem, supervisor)
- Supervision trees and fault tolerance
- Error handling (let it crash philosophy)
- Erlang Term Storage (ETS)
- Distributed Erlang basics
- BEAM VM fundamentals
- Common Erlang idioms
This skill does NOT cover (see specialized skills):
- Advanced OTP patterns →
lang-erlang-patterns-dev - Release management and deployment →
lang-erlang-release-dev - Performance optimization →
lang-erlang-performance-dev - Web frameworks (Cowboy, Phoenix) → framework-specific skills
- Database drivers and ORM →
lang-erlang-database-dev
Quick Reference
| Task | Syntax |
|---|---|
| Define module | |
| Export function | |
| Define function | |
| Pattern match | |
| Spawn process | |
| Send message | |
| Receive message | |
| List comprehension | |
| Anonymous function | |
| Guard clause | |
Skill Routing
Use this table to find the right specialized skill:
| When you need to... | Use this skill |
|---|---|
| Implement advanced OTP patterns | |
| Build releases with rebar3 | |
| Optimize performance and profiling | |
| Work with Cowboy web server | |
| Implement database access | |
Pattern Matching
Basic Patterns
% Variable binding X = 42. {ok, Value} = {ok, 100}. % List patterns [Head | Tail] = [1, 2, 3, 4]. % Head = 1, Tail = [2, 3, 4] [First, Second | Rest] = [a, b, c, d]. % Tuple patterns {Name, Age, Email} = {"Alice", 30, "alice@example.com"}. % Map patterns (Erlang 17+) #{name := Name, age := Age} = #{name => "Bob", age => 25}. % Binary patterns <<A:8, B:8, Rest/binary>> = <<1, 2, 3, 4, 5>>.
Function Clauses
% Multiple clauses with pattern matching factorial(0) -> 1; factorial(N) when N > 0 -> N * factorial(N - 1). % Pattern matching in function heads process_result({ok, Data}) -> {success, Data}; process_result({error, Reason}) -> {failure, Reason}; process_result(unknown) -> {failure, unknown_result}. % List processing sum([]) -> 0; sum([H|T]) -> H + sum(T). length_of([]) -> 0; length_of([_|T]) -> 1 + length_of(T).
Guards
% Type guards is_valid_age(Age) when is_integer(Age), Age >= 0, Age =< 150 -> true; is_valid_age(_) -> false. % Comparison guards max(X, Y) when X > Y -> X; max(_, Y) -> Y. % Multiple guards process(X, Y) when is_number(X); is_number(Y) -> X + Y; process(X, Y) -> {error, not_numbers}. % Built-in guard BIFs is_valid(Value) when is_atom(Value) -> atom; is_valid(Value) when is_list(Value) -> list; is_valid(Value) when is_tuple(Value) -> tuple; is_valid(Value) when is_map(Value) -> map; is_valid(_) -> unknown.
Processes and Concurrency
Spawning Processes
% Basic spawn Pid = spawn(fun() -> loop() end). % Spawn with module/function/args Pid = spawn(my_module, my_function, [Arg1, Arg2]). % Spawn and link (dies together) Pid = spawn_link(fun() -> worker() end). % Spawn and monitor {Pid, Ref} = spawn_monitor(fun() -> task() end).
Message Passing
% Send message Pid ! {self(), hello, "World"}. % Receive messages receive {From, hello, Msg} -> From ! {self(), reply, "Hello " ++ Msg}; {quit} -> ok; Other -> io:format("Unexpected: ~p~n", [Other]) after 5000 -> timeout end. % Selective receive receive {Priority, high, Msg} -> handle_urgent(Msg); {Priority, normal, Msg} -> handle_normal(Msg) end.
Generic Server Loop
-module(simple_server). -export([start/0, loop/1]). start() -> spawn(?MODULE, loop, [#{}]). loop(State) -> receive {From, get, Key} -> Value = maps:get(Key, State, undefined), From ! {self(), Value}, loop(State); {From, put, Key, Value} -> NewState = maps:put(Key, Value, State), From ! {self(), ok}, loop(NewState); {From, stop} -> From ! {self(), stopping}, ok; _ -> loop(State) end.
OTP Behaviors
gen_server
-module(counter_server). -behaviour(gen_server). % API -export([start_link/0, increment/0, get_count/0]). % gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(SERVER, ?MODULE). %%% API Functions start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). increment() -> gen_server:cast(?SERVER, increment). get_count() -> gen_server:call(?SERVER, get_count). %%% gen_server Callbacks init([]) -> {ok, 0}. % Initial state: counter = 0 handle_call(get_count, _From, Count) -> {reply, Count, Count}; handle_call(_Request, _From, State) -> {reply, ok, State}. handle_cast(increment, Count) -> {noreply, Count + 1}; handle_cast(_Msg, State) -> {noreply, State}. handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}.
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, % one_for_one, one_for_all, rest_for_one intensity => 5, % Max restarts period => 60 % Within period (seconds) }, ChildSpecs = [ #{ id => worker1, start => {worker_module, start_link, []}, restart => permanent, % permanent, temporary, transient shutdown => 5000, type => worker, modules => [worker_module] }, #{ id => worker2, start => {another_worker, start_link, [arg1]}, restart => permanent, shutdown => 5000, type => worker, modules => [another_worker] } ], {ok, {SupFlags, ChildSpecs}}.
gen_statem
-module(door). -behaviour(gen_statem). -export([start_link/0, open/0, close/0, lock/0, unlock/0]). -export([callback_mode/0, init/1, locked/3, unlocked/3, open/3, terminate/3]). %%% API start_link() -> gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []). open() -> gen_statem:cast(?MODULE, open). close() -> gen_statem:cast(?MODULE, close). lock() -> gen_statem:cast(?MODULE, lock). unlock() -> gen_statem:cast(?MODULE, unlock). %%% Callbacks callback_mode() -> state_functions. init([]) -> {ok, locked, #{}}. %% State: locked locked(cast, unlock, Data) -> {next_state, unlocked, Data}; locked(_EventType, _Event, Data) -> {keep_state, Data}. %% State: unlocked unlocked(cast, lock, Data) -> {next_state, locked, Data}; unlocked(cast, open, Data) -> {next_state, open, Data}; unlocked(_EventType, _Event, Data) -> {keep_state, Data}. %% State: open open(cast, close, Data) -> {next_state, unlocked, Data}; open(_EventType, _Event, Data) -> {keep_state, Data}. terminate(_Reason, _State, _Data) -> ok.
Error Handling
Let It Crash Philosophy
% Bad: Defensive programming process_data(Data) -> try validate(Data), transform(Data), save(Data) catch error:Reason -> {error, Reason} end. % Good: Let supervisor restart on failure process_data(Data) -> validate(Data), % Crash if invalid transform(Data), % Crash if transform fails save(Data). % Crash if save fails
Try-Catch
% Standard try-catch divide(A, B) -> try A / B of Result -> {ok, Result} catch error:badarith -> {error, division_by_zero}; error:Reason -> {error, Reason} end. % Try-catch with after process_file(Filename) -> {ok, File} = file:open(Filename, [read]), try do_work(File) catch error:Reason -> {error, Reason} after file:close(File) end.
Exit Signals and Links
% Trapping exits process_flag(trap_exit, true), Pid = spawn_link(fun() -> risky_operation() end), receive {'EXIT', Pid, normal} -> ok; {'EXIT', Pid, Reason} -> io:format("Process died: ~p~n", [Reason]) end. % Monitoring (one-way) Pid = spawn(fun() -> worker() end), Ref = monitor(process, Pid), receive {'DOWN', Ref, process, Pid, Reason} -> io:format("Process down: ~p~n", [Reason]) end.
Data Structures
Lists
% List operations List = [1, 2, 3, 4, 5]. [H|T] = List. % H = 1, T = [2,3,4,5] % List comprehensions Squares = [X*X || X <- [1,2,3,4,5]]. % [1,4,9,16,25] Evens = [X || X <- [1,2,3,4,5,6], X rem 2 == 0]. % [2,4,6] % Nested comprehensions Pairs = [{X, Y} || X <- [1,2,3], Y <- [a,b]]. % [{1,a},{1,b},{2,a},{2,b},{3,a},{3,b}] % Common list functions length([1,2,3]). % 3 lists:reverse([1,2,3]). % [3,2,1] lists:sort([3,1,2]). % [1,2,3] 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
Maps
% Creating maps Map1 = #{name => "Alice", age => 30}. Map2 = #{name := "Bob", age := 25}. % := for matching % Accessing values #{name := Name} = Map1. % Name = "Alice" Age = maps:get(age, Map1). % 30 Email = maps:get(email, Map1, "no-email"). % "no-email" (default) % Updating maps Map3 = Map1#{age := 31}. % Update existing key Map4 = Map1#{email => "alice@example.com"}. % Add new key % Map operations maps:keys(Map1). % [name, age] maps:values(Map1). % ["Alice", 30] maps:size(Map1). % 2 maps:is_key(name, Map1). % true maps:remove(age, Map1). % #{name => "Alice"} maps:merge(Map1, #{city => "NYC"}).
Tuples
% Creating tuples Person = {person, "Alice", 30, "alice@example.com"}. % Pattern matching {person, Name, Age, Email} = Person. % Tagged tuples (records alternative) {ok, Value} = {ok, 42}. {error, Reason} = {error, not_found}. % Tuple operations tuple_size(Person). % 4 element(2, Person). % "Alice" setelement(3, Person, 31). % {person, "Alice", 31, "alice@example.com"}
Records
% Define record -record(person, { name :: string(), age :: integer(), email :: string() }). % Create record Person = #person{name="Alice", age=30, email="alice@example.com"}. % Access fields Name = Person#person.name. % "Alice" % Update record Person2 = Person#person{age=31}. % Pattern match record #person{name=Name, age=Age} = Person.
ETS (Erlang Term Storage)
% Create table TableId = ets:new(my_table, [set, public, named_table]). % Table types: set, ordered_set, bag, duplicate_bag % Insert data ets:insert(my_table, {key1, value1}). ets:insert(my_table, [{key2, value2}, {key3, value3}]). % Lookup data [{key1, Value}] = ets:lookup(my_table, key1). % Delete ets:delete(my_table, key1). % Iterate ets:foldl( fun({Key, Value}, Acc) -> io:format("~p: ~p~n", [Key, Value]), Acc end, [], my_table ). % Match patterns Pattern = {'$1', '$2'}, % Any key, any value ets:match(my_table, Pattern). % Clean up ets:delete(my_table).
Distributed Erlang
Node Communication
% Start distributed node % $ erl -name node1@hostname -setcookie secret_cookie % Connect to another node net_adm:ping('node2@hostname'). % pong | pang % List connected nodes nodes(). % Spawn on remote node Pid = spawn('node2@hostname', Module, Function, Args). % Send message to remote process {registered_name, 'node2@hostname'} ! Message. % RPC call rpc:call('node2@hostname', Module, Function, Args).
Distributed Example
-module(distributed_counter). -export([start/0, increment/1, get_value/1]). start() -> register(counter, spawn(fun() -> loop(0) end)). increment(Node) -> {counter, Node} ! {self(), increment}, receive {counter, Value} -> Value end. get_value(Node) -> {counter, Node} ! {self(), get}, receive {counter, Value} -> Value end. loop(Count) -> receive {From, increment} -> NewCount = Count + 1, From ! {counter, NewCount}, loop(NewCount); {From, get} -> From ! {counter, Count}, loop(Count) end.
Common Patterns
Timeout Pattern
call_with_timeout(Pid, Request, Timeout) -> Pid ! {self(), Request}, receive {Pid, Response} -> {ok, Response} after Timeout -> {error, timeout} end.
Worker Pool Pattern
-module(worker_pool). -export([start/1, submit_task/2]). start(PoolSize) -> [spawn(fun() -> worker_loop() end) || _ <- lists:seq(1, PoolSize)]. worker_loop() -> receive {From, Task} -> Result = execute_task(Task), From ! {self(), Result}, worker_loop() end. submit_task(Workers, Task) -> Worker = lists:nth(rand:uniform(length(Workers)), Workers), Worker ! {self(), Task}, receive {Worker, Result} -> Result end.
Pipeline Pattern
pipeline(Data, Stages) -> lists:foldl( fun(Stage, Acc) -> Stage(Acc) end, Data, Stages ). % Usage Result = pipeline( Input, [ fun validate/1, fun transform/1, fun enrich/1, fun save/1 ] ).
Module Structure
-module(my_module). % Module attributes -author("Your Name"). -vsn("1.0.0"). % Behavior declarations -behaviour(gen_server). % Exports -export([ % Public API start_link/0, stop/0, % gen_server callbacks init/1, handle_call/3, handle_cast/2 ]). % Internal exports (for spawn, etc.) -export([internal_function/1]). % Type definitions -type state() :: #{ count := integer(), data := list() }. % Record definitions -record(state, { count = 0 :: integer(), data = [] :: list() }). % Macros -define(DEFAULT_TIMEOUT, 5000). -define(SERVER, ?MODULE). % Include files -include("common.hrl"). -include_lib("kernel/include/file.hrl"). %%% API Functions start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %%% gen_server Callbacks init([]) -> {ok, #state{}}. %%% Internal Functions internal_function(Arg) -> Arg * 2.
Debugging and Tracing
% Debug messages io:format("Debug: ~p~n", [Variable]). % Process info process_info(Pid). process_info(Pid, messages). process_info(Pid, memory). % System info erlang:system_info(process_count). erlang:system_info(schedulers). % Basic tracing dbg:tracer(). dbg:p(all, c). % Trace all processes, calls dbg:tpl(Module, Function, Arity, []). dbg:stop_clear(). % Observer (GUI tool) % $ erl % 1> observer:start().
Troubleshooting
Process Not Receiving Messages
- Check process is alive:
is_process_alive(Pid) - Verify message format: Ensure sender/receiver patterns match
- Check mailbox:
process_info(Pid, messages) - Look for selective receive: Messages might be queued
Supervisor Keeps Restarting Child
- Check init/1 return: Must be
or{ok, State}{ok, State, Timeout} - Verify start_link: Must return
or{ok, Pid}{ok, Pid, Info} - Review intensity/period: May be restarting too frequently
- Check logs: Look for crash reasons
Pattern Match Failures
- Use catch: Wrap in try-catch to see actual value
- Print before match:
io:format("Value: ~p~n", [Value]) - Check types: Ensure atoms vs strings vs binaries
- Review guards: Guards fail silently
Performance Issues
- Avoid list concatenation: Use
sparingly, prefer++[H|T] - Use ETS for shared state: Don't pass large data in messages
- Profile with fprof:
fprof:apply(Module, Function, Args) - Check for process bottlenecks: Use observer to find message queue buildup
Testing
EUnit (Unit Testing)
-module(calculator_tests). -include_lib("eunit/include/eunit.hrl"). %% Simple test simple_add_test() -> ?assertEqual(5, calculator:add(2, 3)). %% Test with setup/cleanup setup_test_() -> {setup, fun() -> setup() end, % Setup fun(_) -> cleanup() end, % Cleanup fun(_) -> ?assertEqual(42, get_value()) end }. %% Test fixtures (multiple tests with same setup) calculator_test_() -> {foreach, fun setup/0, fun cleanup/1, [ fun test_addition/1, fun test_subtraction/1, fun test_multiplication/1 ]}. test_addition(_State) -> ?_assertEqual(5, calculator:add(2, 3)). test_subtraction(_State) -> ?_assertEqual(1, calculator:subtract(3, 2)). test_multiplication(_State) -> ?_assertEqual(6, calculator:multiply(2, 3)). %% Assertions assertions_test() -> % Equality ?assertEqual(Expected, Actual), ?assertNotEqual(NotExpected, Actual), % Boolean ?assert(true), ?assertNot(false), % Exceptions ?assertException(error, badarith, 1/0), ?assertError(badarg, list_to_integer("not_a_number")), ?assertThrow(my_exception, throw(my_exception)), ?assertExit(normal, exit(normal)), % Pattern matching ?assertMatch({ok, _}, {ok, 42}), ?assertMatch([H|_] when H > 0, [1, 2, 3]). %% Generator test (multiple test cases) divide_test_() -> [ ?_assertEqual(2, calculator:divide(6, 3)), ?_assertEqual(5, calculator:divide(10, 2)), ?_assertError(badarith, calculator:divide(10, 0)) ]. %% Test descriptions named_tests_test_() -> [ {"Addition of positive numbers", ?_assertEqual(5, calculator:add(2, 3))}, {"Addition with negative numbers", ?_assertEqual(-1, calculator:add(-3, 2))}, {"Addition with zero", ?_assertEqual(5, calculator:add(5, 0))} ].
Common Test (Integration Testing)
-module(database_SUITE). -include_lib("common_test/include/ct.hrl"). %% CT callbacks -export([all/0, groups/0, init_per_suite/1, end_per_suite/1, init_per_group/2, end_per_group/2, init_per_testcase/2, end_per_testcase/2]). %% Test cases -export([test_insert/1, test_query/1, test_delete/1, test_transaction/1, test_concurrent_access/1]). %% Define all test cases all() -> [ {group, basic_operations}, {group, advanced_operations} ]. %% Define test groups groups() -> [ {basic_operations, [sequence], [test_insert, test_query, test_delete]}, {advanced_operations, [parallel], [test_transaction, test_concurrent_access]} ]. %% Suite-level setup (runs once) init_per_suite(Config) -> application:start(database), [{db_conn, database:connect()} | Config]. end_per_suite(Config) -> Conn = ?config(db_conn, Config), database:disconnect(Conn), application:stop(database), ok. %% Group-level setup init_per_group(basic_operations, Config) -> [{table, create_test_table()} | Config]; init_per_group(advanced_operations, Config) -> [{table, create_test_table()}, {pool_size, 10} | Config]. end_per_group(_GroupName, Config) -> Table = ?config(table, Config), drop_table(Table), ok. %% Test case setup/teardown init_per_testcase(TestCase, Config) -> ct:log("Starting test: ~p", [TestCase]), Config. end_per_testcase(TestCase, _Config) -> ct:log("Finished test: ~p", [TestCase]), ok. %% Test cases test_insert(Config) -> Conn = ?config(db_conn, Config), {ok, Id} = database:insert(Conn, #{name => "Alice", age => 30}), true = is_integer(Id). test_query(Config) -> Conn = ?config(db_conn, Config), {ok, Results} = database:query(Conn, "SELECT * FROM users"), true = length(Results) > 0. test_delete(Config) -> Conn = ?config(db_conn, Config), ok = database:delete(Conn, 1), {ok, []} = database:find(Conn, 1). test_transaction(Config) -> Conn = ?config(db_conn, Config), ok = database:transaction(Conn, fun() -> database:insert(Conn, #{name => "Bob"}), database:insert(Conn, #{name => "Charlie"}) end). test_concurrent_access(Config) -> Conn = ?config(db_conn, Config), PoolSize = ?config(pool_size, Config), % Spawn concurrent workers Pids = [spawn_link(fun() -> worker(Conn) end) || _ <- lists:seq(1, PoolSize)], % Wait for all to complete [receive {done, Pid} -> ok end || Pid <- Pids], ok. worker(Conn) -> database:insert(Conn, #{data => rand:uniform(1000)}), self() ! {done, self()}.
PropEr (Property-Based Testing)
-module(list_props). -include_lib("proper/include/proper.hrl"). %% Property: reversing a list twice gives the original list prop_reverse_twice() -> ?FORALL(List, list(integer()), lists:reverse(lists:reverse(List)) =:= List). %% Property: length is preserved after reversing prop_reverse_length() -> ?FORALL(List, list(any()), length(lists:reverse(List)) =:= length(List)). %% Property: appending and reversing prop_append_reverse() -> ?FORALL({L1, L2}, {list(integer()), list(integer())}, lists:reverse(L1 ++ L2) =:= lists:reverse(L2) ++ lists:reverse(L1)). %% Property with generators prop_positive_sum() -> ?FORALL(List, non_empty(list(positive_integer())), lists:sum(List) > 0). %% Custom generators id() -> ?LET(N, range(1, 1000000), N). user() -> ?LET({Name, Age}, {non_empty(string()), range(0, 150)}, #{name => Name, age => Age}). prop_user_validation() -> ?FORALL(User, user(), validator:is_valid_user(User)). %% Stateful property testing prop_counter_stateful() -> ?FORALL(Cmds, commands(?MODULE), begin {ok, Pid} = counter:start_link(), {History, State, Result} = run_commands(?MODULE, Cmds), counter:stop(Pid), ?WHENFAIL( io:format("History: ~p\nState: ~p\nResult: ~p\n", [History, State, Result]), Result =:= ok ) end).
Mocking with Meck
-module(user_service_tests). -include_lib("eunit/include/eunit.hrl"). %% Test with mocking mock_database_test() -> % Setup mock meck:new(database, [non_strict]), meck:expect(database, find, fun(1) -> {ok, #{id => 1, name => "Alice"}} end), % Test {ok, User} = user_service:get_user(1), ?assertEqual("Alice", maps:get(name, User)), % Verify mock was called ?assert(meck:called(database, find, [1])), % Cleanup meck:unload(database). %% Test with multiple mocks multiple_mocks_test() -> meck:new([database, cache], [non_strict]), meck:expect(cache, get, fun(_Key) -> {error, not_found} end), meck:expect(database, find, fun(1) -> {ok, #{id => 1}} end), meck:expect(cache, put, fun(_Key, _Value) -> ok end), % Service should check cache, then database, then update cache {ok, User} = user_service:get_user(1), ?assert(meck:called(cache, get, [1])), ?assert(meck:called(database, find, [1])), ?assert(meck:called(cache, put, [1, User])), meck:unload([database, cache]). %% Passthrough mocking (partial mocking) passthrough_test() -> meck:new(mymodule, [passthrough]), % Mock only specific function meck:expect(mymodule, expensive_function, fun() -> cached_result end), % Other functions work normally Result = mymodule:normal_function(), meck:unload(mymodule). %% Sequence mocking (different returns per call) sequence_test() -> meck:new(external_api, [non_strict]), meck:sequence(external_api, fetch_data, 0, [ {error, timeout}, {error, timeout}, {ok, data} ]), % First two calls fail, third succeeds {error, timeout} = external_api:fetch_data(), {error, timeout} = external_api:fetch_data(), {ok, data} = external_api:fetch_data(), meck:unload(external_api).
Test Fixtures and Setup
-module(fixture_examples). -include_lib("eunit/include/eunit.hrl"). %% Simple setup/cleanup simple_fixture_test_() -> {setup, fun() -> Pid = setup_server(), Pid end, fun(Pid) -> stop_server(Pid) end, fun(Pid) -> [ ?_assertEqual(ok, call_server(Pid, ping)), ?_assertEqual({ok, 42}, call_server(Pid, get_value)) ] end }. %% Foreach (setup for each test) foreach_fixture_test_() -> {foreach, fun setup/0, fun cleanup/1, [ fun test_case_1/1, fun test_case_2/1, fun test_case_3/1 ] }. setup() -> {ok, Pid} = my_server:start_link(), Pid. cleanup(Pid) -> my_server:stop(Pid). test_case_1(Pid) -> ?_assertEqual(ok, my_server:call(Pid, command1)). %% Fixtures with state stateful_fixture_test_() -> {foreach, fun() -> ets:new(test_table, [named_table, public]), ets:insert(test_table, {key1, value1}), test_table end, fun(Table) -> ets:delete(Table) end, [ fun(Table) -> ?_assertEqual([{key1, value1}], ets:lookup(Table, key1)) end ] }. %% Nested fixtures nested_fixture_test_() -> {setup, fun global_setup/0, fun global_cleanup/1, {foreach, fun per_test_setup/0, fun per_test_cleanup/1, [ fun test_with_both_fixtures/1 ] } }.
Serialization
JSON with jiffy
% Using jiffy (fast NIF-based JSON library) % Add to rebar.config: {deps, [{jiffy, "1.1.1"}]} % Encode to JSON encode_user(User) -> jiffy:encode(#{ <<"name">> => maps:get(name, User), <<"age">> => maps:get(age, User), <<"email">> => maps:get(email, User) }). % Decode from JSON decode_user(JsonBinary) -> case jiffy:decode(JsonBinary, [return_maps]) of #{<<"name">> := Name, <<"age">> := Age, <<"email">> := Email} -> {ok, #{name => Name, age => Age, email => Email}}; _ -> {error, invalid_format} end. % Pretty printing pretty_json(Term) -> jiffy:encode(Term, [pretty]). % Handling JSON arrays decode_users(JsonBinary) -> case jiffy:decode(JsonBinary, [return_maps]) of Users when is_list(Users) -> {ok, [decode_user_map(U) || U <- Users]}; _ -> {error, expected_array} end.
JSON with jsx
% Using jsx (pure Erlang JSON library) % Add to rebar.config: {deps, [{jsx, "3.1.0"}]} % Encode to JSON encode_event(Event) -> jsx:encode([ {<<"type">>, maps:get(type, Event)}, {<<"timestamp">>, maps:get(timestamp, Event)}, {<<"data">>, maps:get(data, Event)} ]). % Decode from JSON decode_event(JsonBinary) -> try jsx:decode(JsonBinary, [return_maps]) of Map when is_map(Map) -> {ok, Map}; _ -> {error, invalid_json} catch error:badarg -> {error, parse_error} end. % Streaming JSON (for large files) parse_json_stream(Binary) -> jsx:decode(Binary, [stream, return_maps]).
Erlang Term Format (ETF)
% Native binary serialization (fastest for Erlang-to-Erlang) % Serialize term to binary serialize(Term) -> term_to_binary(Term). % Serialize with compression serialize_compressed(Term) -> term_to_binary(Term, [compressed]). % Deserialize binary to term deserialize(Binary) -> binary_to_term(Binary). % Safe deserialization (prevents atom table exhaustion) safe_deserialize(Binary) -> binary_to_term(Binary, [safe]). % File storage example save_state(Filename, State) -> Binary = term_to_binary(State, [compressed]), file:write_file(Filename, Binary). load_state(Filename) -> case file:read_file(Filename) of {ok, Binary} -> {ok, binary_to_term(Binary, [safe])}; Error -> Error end.
Protocol Buffers with gpb
% Using gpb (Google Protocol Buffers for Erlang) % Add to rebar.config: {deps, [{gpb, "4.19.0"}]} % Define .proto file: user.proto % message User { % string name = 1; % int32 age = 2; % string email = 3; % } % After compilation, use generated module encode_user_proto(Name, Age, Email) -> user_pb:encode_msg(#{ name => Name, age => Age, email => Email }, 'User'). decode_user_proto(Binary) -> user_pb:decode_msg(Binary, 'User'). % Rebar3 plugin configuration % {plugins, [rebar3_gpb_plugin]}. % {gpb_opts, [ % {i, "proto"}, % {o_erl, "src"}, % {o_hrl, "include"}, % {module_name_suffix, "_pb"} % ]}.
MessagePack with msgpack
% Using msgpack-erlang (compact binary format) % Add to rebar.config: {deps, [{msgpack, "0.7.0"}]} % Pack data pack_data(Data) -> msgpack:pack(Data). % Unpack data unpack_data(Binary) -> case msgpack:unpack(Binary) of {ok, Term} -> {ok, Term}; {error, Reason} -> {error, Reason} end. % Options for map handling pack_with_options(Data) -> msgpack:pack(Data, [{map_format, map}]). % Use maps instead of proplists
Build and Dependencies
Rebar3 Basics
% rebar.config - Main configuration file {erl_opts, [ debug_info, {parse_transform, lager_transform}, warnings_as_errors ]}. {deps, [ {cowboy, "2.10.0"}, {jsx, "3.1.0"}, {lager, "3.9.2"}, % Git dependency {custom_lib, {git, "https://github.com/user/custom_lib.git", {tag, "v1.0.0"}}}, % Hex dependency with specific version {hackney, "1.18.1"} ]}. {shell, [ {apps, [my_app]} ]}. % Profiles for different environments {profiles, [ {test, [ {deps, [ {meck, "0.9.2"}, {proper, "1.4.0"} ]}, {erl_opts, [nowarn_export_all]} ]}, {prod, [ {relx, [ {dev_mode, false}, {include_erts, true} ]} ]} ]}.
Common Rebar3 Commands
# Create new project rebar3 new app my_app rebar3 new lib my_lib rebar3 new release my_release # Compile rebar3 compile # Run tests rebar3 eunit rebar3 ct rebar3 proper # If using PropEr # Start shell with application rebar3 shell # Build release rebar3 release rebar3 as prod release # Production release # Dependency management rebar3 deps rebar3 tree # Show dependency tree rebar3 upgrade # Upgrade dependencies rebar3 lock # Update lock file # Dialyzer (static analysis) rebar3 dialyzer # Documentation rebar3 edoc # Clean rebar3 clean rebar3 clean --all # Including dependencies
Hex Package Management
% Publishing to Hex.pm % In rebar.config, add metadata: {hex, [ {doc, #{provider => ex_doc}} ]}. {project_plugins, [rebar3_hex]}. % Package metadata in src/my_app.app.src {application, my_app, [ {description, "My awesome Erlang application"}, {vsn, "1.0.0"}, {registered, []}, {mod, {my_app, []}}, {applications, [kernel, stdlib, cowboy]}, {env, []}, {licenses, ["Apache-2.0"]}, {links, [{"GitHub", "https://github.com/user/my_app"}]} ]}.
# Hex commands rebar3 hex user register # Register account rebar3 hex user auth # Authenticate rebar3 hex publish # Publish package rebar3 hex retire my_app 1.0.0 --reason security # Retire version rebar3 hex search cowboy # Search packages rebar3 hex info cowboy # Package info
Release Management with Relx
% Release configuration in rebar.config {relx, [ {release, {my_app, "1.0.0"}, [ my_app, sasl, runtime_tools ]}, {mode, dev}, % dev | minimal | prod % System configuration {sys_config, "./config/sys.config"}, {vm_args, "./config/vm.args"}, % Extended start script {extended_start_script, true}, % Overlay for additional files {overlay, [ {mkdir, "log"}, {mkdir, "data"}, {copy, "priv/static", "priv/static"}, {template, "config/app.config.template", "releases/{{release_version}}/app.config"} ]} ]}. % Production profile {profiles, [ {prod, [ {relx, [ {mode, prod}, {dev_mode, false}, {include_erts, true}, {include_src, false} ]} ]} ]}.
# Release commands rebar3 release # Build release rebar3 tar # Create tarball rebar3 as prod release # Production release rebar3 as prod tar # Production tarball # Running release _build/default/rel/my_app/bin/my_app console # Interactive _build/default/rel/my_app/bin/my_app daemon # Background _build/default/rel/my_app/bin/my_app stop # Stop _build/default/rel/my_app/bin/my_app remote_console # Attach to running
Application Configuration
% config/sys.config [ {my_app, [ {port, 8080}, {pool_size, 10}, {database, [ {host, "localhost"}, {port, 5432}, {name, "my_db"} ]} ]}, {lager, [ {handlers, [ {lager_console_backend, [{level, info}]}, {lager_file_backend, [{file, "log/error.log"}, {level, error}]} ]} ]} ]. % config/vm.args -name my_app@127.0.0.1 -setcookie my_secret_cookie +K true +A 64 -env ERL_MAX_PORTS 65536 -env ERL_FULLSWEEP_AFTER 10 % Accessing configuration in code get_config() -> Port = application:get_env(my_app, port, 8080), PoolSize = application:get_env(my_app, pool_size, 5), {Port, PoolSize}.
Metaprogramming
Parse Transforms
% Parse transforms modify AST at compile time % my_transform.erl -module(my_transform). -export([parse_transform/2]). parse_transform(Forms, _Options) -> [transform_form(Form) || Form <- Forms]. transform_form({function, Line, Name, Arity, Clauses}) -> % Transform function definitions NewClauses = [transform_clause(C) || C <- Clauses], {function, Line, Name, Arity, NewClauses}; transform_form(Form) -> Form. transform_clause({clause, Line, Patterns, Guards, Body}) -> % Add logging to every function LogCall = {call, Line, {remote, Line, {atom, Line, io}, {atom, Line, format}}, [{string, Line, "Entering function~n"}, {nil, Line}] }, {clause, Line, Patterns, Guards, [LogCall | Body]}. % Using the transform % In module that uses it: -compile({parse_transform, my_transform}).
Compile-Time Code Generation
% Generate code based on configuration -module(codegen_transform). -export([parse_transform/2]). parse_transform(Forms, Options) -> Config = proplists:get_value(config, Options, []), inject_config_functions(Forms, Config). inject_config_functions(Forms, Config) -> ConfigFuns = [generate_getter(Key, Value) || {Key, Value} <- Config], % Insert before final form (usually eof) insert_before_eof(Forms, ConfigFuns). generate_getter(Key, Value) -> {function, 1, Key, 0, [ {clause, 1, [], [], [erl_parse:abstract(Value)]} ]}. insert_before_eof([{eof, Line}], Funs) -> Funs ++ [{eof, Line}]; insert_before_eof([H|T], Funs) -> [H | insert_before_eof(T, Funs)].
Macro Definitions
% Macros defined with -define % Simple value macro -define(MAX_RETRIES, 3). -define(TIMEOUT, 5000). % Macro with arguments -define(LOG(Msg), io:format("[~p] ~s~n", [?MODULE, Msg])). -define(LOG(Fmt, Args), io:format("[~p] " ++ Fmt ++ "~n", [?MODULE | Args])). % Multi-line macro -define(WITH_RETRY(Expr), (fun Loop(0) -> {error, max_retries}; Loop(N) -> case Expr of {error, _} -> timer:sleep(100), Loop(N - 1); Result -> Result end end)(?MAX_RETRIES) ). % Conditional compilation -ifdef(TEST). -define(DEBUG(Msg), io:format("DEBUG: ~p~n", [Msg])). -else. -define(DEBUG(Msg), ok). -endif. % Predefined macros example() -> io:format("Module: ~p~n", [?MODULE]), io:format("Function: ~p~n", [?FUNCTION_NAME]), io:format("Arity: ~p~n", [?FUNCTION_ARITY]), io:format("Line: ~p~n", [?LINE]), io:format("File: ~s~n", [?FILE]). % Using macros process_with_retry(Data) -> ?WITH_RETRY(external_service:call(Data)).
Record Introspection
% Record information at compile time -record(user, {id, name, email, created_at}). % Record field introspection get_user_fields() -> record_info(fields, user). % Returns [id, name, email, created_at] get_user_size() -> record_info(size, user). % Returns 5 (1 + number of fields) % Dynamic record access (at compile time) get_field(#user{} = User, Field) -> element(field_index(Field), User). field_index(id) -> #user.id; field_index(name) -> #user.name; field_index(email) -> #user.email; field_index(created_at) -> #user.created_at.
Behaviors as Metaprogramming
% Define custom behavior -module(my_handler). % Behavior callback specifications -callback init(Args :: term()) -> {ok, State :: term()} | {error, Reason :: term()}. -callback handle(Request :: term(), State :: term()) -> {reply, Response :: term(), NewState :: term()}. -callback terminate(State :: term()) -> ok. % Optional callbacks -optional_callbacks([terminate/1]). % Implementation module -module(my_impl). -behaviour(my_handler). -export([init/1, handle/2, terminate/1]). init(Args) -> {ok, #{args => Args}}. handle(Request, State) -> Response = process(Request), {reply, Response, State}. terminate(_State) -> ok. % Behavior verification at compile time happens automatically % Missing required callbacks cause compilation warnings
Zero and Default Values
Default Value Patterns
% Erlang has no null - uses atoms like 'undefined' or tagged tuples % Pattern 1: undefined atom (common convention) -record(config, { host = "localhost" :: string(), port = 8080 :: integer(), timeout = undefined :: integer() | undefined, ssl = false :: boolean() }). get_timeout(#config{timeout = undefined}) -> 5000; % Default when undefined get_timeout(#config{timeout = Timeout}) -> Timeout. % Pattern 2: Option with default get_opt(Key, Options, Default) -> case proplists:get_value(Key, Options) of undefined -> Default; Value -> Value end. % Same for maps get_map_opt(Key, Map, Default) -> maps:get(Key, Map, Default). % Built-in default support % Pattern 3: Maybe pattern (explicit optionality) -type maybe(T) :: {just, T} | nothing. find_user(Id, Users) -> case lists:keyfind(Id, 1, Users) of {Id, User} -> {just, User}; false -> nothing end. handle_user(Id, Users) -> case find_user(Id, Users) of {just, User} -> process_user(User); nothing -> {error, not_found} end.
Default Function Arguments
% Erlang doesn't have default arguments - use multiple clauses or options % Pattern 1: Multiple function clauses connect(Host) -> connect(Host, 5432). connect(Host, Port) -> connect(Host, Port, []). connect(Host, Port, Options) -> Timeout = proplists:get_value(timeout, Options, 5000), Pool = proplists:get_value(pool_size, Options, 10), do_connect(Host, Port, Timeout, Pool). % Pattern 2: Options proplist/map start_server(Options) -> Host = maps:get(host, Options, "0.0.0.0"), Port = maps:get(port, Options, 8080), Workers = maps:get(workers, Options, erlang:system_info(schedulers)), do_start(Host, Port, Workers). % Usage start_server(#{port => 9000}). % Uses defaults for host and workers % Pattern 3: Record with defaults -record(request_opts, { method = get :: atom(), headers = [] :: [{string(), string()}], body = <<>> :: binary(), timeout = 30000 :: integer() }). http_request(Url) -> http_request(Url, #request_opts{}). http_request(Url, Opts) when is_record(Opts, request_opts) -> #request_opts{ method = Method, headers = Headers, body = Body, timeout = Timeout } = Opts, do_request(Url, Method, Headers, Body, Timeout).
Handling Missing Values
% Safe access patterns % Maps - use maps:get/3 for default safe_get_name(User) -> maps:get(name, User, <<"Anonymous">>). % Maps - use maps:find/2 for explicit handling handle_email(User) -> case maps:find(email, User) of {ok, Email} -> send_notification(Email); error -> skip_notification end. % Proplist - use proplists:get_value/3 get_setting(Key, Settings) -> proplists:get_value(Key, Settings, default_value(Key)). % ETS - handle missing keys ets_get_or_default(Table, Key, Default) -> case ets:lookup(Table, Key) of [{Key, Value}] -> Value; [] -> Default end. % Pattern: with_default combinator with_default(Fun, Default) -> try Fun() of Result -> Result catch _:_ -> Default end. % Usage Value = with_default(fun() -> expensive_computation() end, cached_value).
Initialization Patterns
% GenServer initialization with defaults -record(state, { counter = 0 :: integer(), cache = #{} :: map(), config :: map() % Required, no default }). init(Args) -> Config = maps:get(config, Args), % Required InitialCounter = maps:get(initial_counter, Args, 0), CacheSize = maps:get(cache_size, Args, 1000), State = #state{ counter = InitialCounter, cache = init_cache(CacheSize), config = Config }, {ok, State}. % Application environment defaults get_app_config(Key, Default) -> application:get_env(my_app, Key, Default). init_from_env() -> Port = get_app_config(port, 8080), Host = get_app_config(host, "localhost"), Workers = get_app_config(workers, erlang:system_info(schedulers) * 2), {Port, Host, Workers}.
Empty/Zero Values by Type
% Canonical empty/zero values for each type % Numbers zero_integer() -> 0. zero_float() -> 0.0. % Strings/Binaries empty_string() -> "". empty_binary() -> <<>>. % Collections empty_list() -> []. empty_map() -> #{}. empty_tuple() -> {}. % Custom initialization helper default_value(integer) -> 0; default_value(float) -> 0.0; default_value(string) -> ""; default_value(binary) -> <<>>; default_value(list) -> []; default_value(map) -> #{}; default_value(boolean) -> false; default_value(atom) -> undefined; default_value({list, _Type}) -> []; default_value({map, _KeyType, _ValueType}) -> #{}. % Initialize record fields with type-based defaults init_record_field(Type, ProvidedValue) -> case ProvidedValue of undefined -> default_value(Type); Value -> Value end.
Cross-Cutting Patterns
For cross-language comparison and translation patterns, see:
- Process patterns, message passing, supervisorspatterns-concurrency-dev
- Unit testing, property-based testing, mocking strategiespatterns-testing-dev
- Parse transforms, macros, behaviorspatterns-metaprogramming-dev
References
- Erlang/OTP Documentation
- Learn You Some Erlang
- Erlang Design Principles
- EUnit User's Guide
- Common Test User's Guide
- PropEr Documentation
- Meck GitHub Repository
- Advanced design patternslang-erlang-patterns-dev
- Release managementlang-erlang-release-dev
- Performance optimizationlang-erlang-performance-dev