Skip to content
lang-erlang-dev logo

Erlang Fundamentals

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. Th...

SKILL.md

Full skill instructions

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

TaskSyntax
Define module-module(module_name).
Export function-export([function_name/​arity]).
Define functionfunction_name(Args) -> Body.
Pattern match{ok, Value} = Result
Spawn processspawn(Module, Function, Args)
Send messagePid ! Message
Receive messagereceive Pattern -> Body end
List comprehension[X*2 || X <- List]
Anonymous functionfun(X) -> X * 2 end
Guard clausewhen is_integer(X)

Skill Routing

Use this table to find the right specialized skill:

When you need to...Use this skill
Implement advanced OTP patternslang-erlang-patterns-dev
Build releases with rebar3lang-erlang-release-dev
Optimize performance and profilinglang-erlang-performance-dev
Work with Cowboy web serverweb-cowboy-dev
Implement database accesslang-erlang-database-dev

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, "[email protected]"}.

% 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 => "[email protected]"}.  % 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, "[email protected]"}.

% 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, "[email protected]"}

Records

% Define record
-record(person, {
    name :: string(),
    age :: integer(),
    email :: string()
}).

% Create record
Person = #person{name="Alice", age=30, email="[email protected]"}.

% 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

  1. Check process is alive: is_process_alive(Pid)
  2. Verify message format: Ensure sender/​receiver patterns match
  3. Check mailbox: process_info(Pid, messages)
  4. Look for selective receive: Messages might be queued

Supervisor Keeps Restarting Child

  1. Check init/​1 return: Must be {ok, State} or {ok, State, Timeout}
  2. Verify start_link: Must return {ok, Pid} or {ok, Pid, Info}
  3. Review intensity/​period: May be restarting too frequently
  4. Check logs: Look for crash reasons

Pattern Match Failures

  1. Use catch: Wrap in try-catch to see actual value
  2. Print before match: io:format("Value: ~p~n", [Value])
  3. Check types: Ensure atoms vs strings vs binaries
  4. Review guards: Guards fail silently

Performance Issues

  1. Avoid list concatenation: Use ++ sparingly, prefer [H|T]
  2. Use ETS for shared state: Don't pass large data in messages
  3. Profile with fprof: fprof:apply(Module, Function, Args)
  4. 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 [email protected]
-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:

  • patterns-concurrency-dev - Process patterns, message passing, supervisors
  • patterns-testing-dev - Unit testing, property-based testing, mocking strategies
  • patterns-metaprogramming-dev - Parse transforms, macros, behaviors

References

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View