Skip to content
convert-elm-erlang logo

Convert Elm to Erlang

convert-elm-erlang

Convert Elm code to idiomatic Erlang/OTP. Use when migrating Elm frontend applications to Erlang backend services, translating Elm patterns to OTP behaviors, or refactoring functional code to BEAM VM. Extends meta-convert-dev with Elm-to-Erlang specific patterns.

SKILL.md

Full skill instructions

Convert Elm to Erlang

Convert Elm code to idiomatic Erlang/​OTP. This skill extends meta-convert-dev with Elm-to-Erlang specific type mappings, idiom translations, and architectural patterns for moving from pure functional frontend code to fault-tolerant backend systems.

This Skill Extends

  • meta-convert-dev - Foundational conversion patterns (APTV workflow, testing strategies)

For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.

This Skill Adds

  • Type mappings: Elm types → Erlang types (records, maps, atoms)
  • Idiom translations: The Elm Architecture (TEA) → OTP behaviors (gen_server, gen_statem)
  • Error handling: Elm Result/​Maybe → Erlang tuples and let-it-crash
  • Concurrency: Elm Cmd/​Sub → Erlang processes and message passing
  • Architecture: Pure functions → Supervised process trees

This Skill Does NOT Cover

  • General conversion methodology - see meta-convert-dev
  • Elm language fundamentals - see lang-elm-dev
  • Erlang language fundamentals - see lang-erlang-dev
  • Reverse conversion (Erlang → Elm) - see convert-erlang-elm

Quick Reference

ElmErlangNotes
Stringbinary() or list()Use binaries for efficiency
Intinteger()Direct mapping
Floatfloat()Direct mapping
Booltrue / falseAtoms in Erlang
List a[a]Direct mapping
Maybe a{ok, a} / undefined / errorTagged tuples
Result e a{ok, a} / {error, e}Standard Erlang convention
type alias-record() or map()Records for structure
type MsgMessage patternsProcess mailbox messages
Modelgen_server stateOTP state management
update functionhandle_call/​castOTP callbacks
Cmd Msgspawn / ! / gen_server:callProcess operations
Sub Msgreceive / timersMessage reception

When Converting Code

  1. Analyze Elm architecture - Understand TEA structure (Model, View, Update, Subscriptions)
  2. Map to OTP behaviors - Model → gen_server state, Msg → messages, update → handle_call/​cast
  3. Preserve pure functions - Keep business logic pure, wrap in processes
  4. Adopt OTP patterns - Don't transliterate; use supervision trees and fault tolerance
  5. Handle guarantees - Elm's no-runtime-errors → Erlang's let-it-crash
  6. Test equivalence - Property-based testing for both languages

Type System Mapping

Primitive Types

ElmErlangNotes
Stringbinary()UTF-8 binary: <<"Hello">>
Stringstring()List of codepoints: "Hello" (less efficient)
Intinteger()Arbitrary precision
Floatfloat()IEEE 754 double precision
Booltrue / falseAtoms, not a separate type
Charinteger()Unicode codepoint
() (unit)ok / undefinedAtom for no value

Collection Types

ElmErlangNotes
List a[a]Linked list
Array aarray:array(a)Fixed-size arrays (rare)
Dict k vmaps:map(k, v)Modern maps (Erlang 17+)
Dict k vdict:dict(k, v)Legacy dict module
Set asets:set(a)Set data structure
(a, b){a, b}Tuple (fixed size)
(a, b, c){a, b, c}Tuple with 3+ elements

Composite Types

ElmErlangNotes
type alias User = { name : String, age : Int }-record(user, {name :: binary(), age :: integer()}).Record with type specs
type alias User = { name : String, age : Int }#{name => binary(), age => integer()}Map (more flexible)
type Msg = Increment | DecrementMessage patterns in receivePatterns, not types
type Result e a = Ok a | Err e{ok, a} | {error, e}Tagged tuples
type Maybe a = Just a | Nothing{ok, a} | undefined | {error, not_found}Multiple conventions

Union Types → Pattern Matching

Elm:

type TrafficLight
    = Red
    | Yellow
    | Green

canGo : TrafficLight -> Bool
canGo light =
    case light of
        Green -> True
        Yellow -> False
        Red -> False

Erlang:

% Define as atoms or tagged tuples
can_go(green) -> true;
can_go(yellow) -> false;
can_go(red) -> false.

% Or with more structure
can_go(Light) ->
    case Light of
        green -> true;
        yellow -> false;
        red -> false
    end.

Why this translation:

  • Elm's union types become atoms or tagged tuples in Erlang
  • Pattern matching in function heads is idiomatic in both languages
  • Erlang doesn't enforce exhaustiveness at compile time (use dialyzer)

Idiom Translation

Pattern 1: The Elm Architecture → gen_server

Elm:

type alias Model = { count : Int }

type Msg
    = Increment
    | Decrement

init : () -> ( Model, Cmd Msg )
init _ = ( { count = 0 }, Cmd.none )

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Increment -> ( { model | count = model.count + 1 }, Cmd.none )
        Decrement -> ( { model | count = model.count - 1 }, Cmd.none )

Erlang:

-module(counter).
-behaviour(gen_server).

-export([start_link/​0, increment/​0, decrement/​0, get_count/​0]).
-export([init/​1, handle_call/​3, handle_cast/​2, handle_info/​2, terminate/​2, code_change/​3]).

-record(state, {count = 0 :: integer()}).

%%% API
start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

increment() ->
    gen_server:cast(?MODULE, increment).

decrement() ->
    gen_server:cast(?MODULE, decrement).

get_count() ->
    gen_server:call(?MODULE, get_count).

%%% Callbacks
init([]) ->
    {ok, #state{count = 0}}.

handle_call(get_count, _From, State) ->
    {reply, State#state.count, State}.

handle_cast(increment, State = #state{count = Count}) ->
    {noreply, State#state{count = Count + 1}};
handle_cast(decrement, State = #state{count = Count}) ->
    {noreply, State#state{count = Count - 1}}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

Why this translation:

  • Elm's Model → gen_server state record
  • Elm's Msg type → message patterns in handle_cast/​handle_call
  • Elm's update function → handle_cast/​handle_call callbacks
  • Elm's init → gen_server init/​1 callback
  • gen_server provides supervision, hot code reloading, and OTP integration

Pattern 2: Maybe/​Result → Tagged Tuples

Elm:

findUser : Int -> Maybe User
findUser id =
    if id == 1 then
        Just { name = "Alice", age = 30 }
    else
        Nothing

name : String
name =
    findUser 1
        |> Maybe.map .name
        |> Maybe.withDefault "Anonymous"

Erlang:

find_user(Id) ->
    case Id of
        1 -> {ok, #{name => <<"Alice">>, age => 30}};
        _ -> {error, not_found}
    end.

% Using case for pattern matching
get_name() ->
    case find_user(1) of
        {ok, User} -> maps:get(name, User);
        {error, _} -> <<"Anonymous">>
    end.

% Or with function clauses
get_name_clause({ok, User}) -> maps:get(name, User);
get_name_clause({error, _}) -> <<"Anonymous">>.

Why this translation:

  • Nothing → {error, Reason} or undefined
  • Just value → {ok, Value}
  • Erlang convention: {ok, Result} for success, {error, Reason} for failure
  • Pattern matching in case or function clauses replaces Maybe combinators

Pattern 3: List Operations

Elm:

result : Int
result =
    [1, 2, 3, 4, 5]
        |> List.filter (\x -> x > 2)
        |> List.map (\x -> x * 2)
        |> List.foldl (+) 0

Erlang:

result() ->
    lists:foldl(
        fun(X, Acc) -> X + Acc end,
        0,
        lists:map(
            fun(X) -> X * 2 end,
            lists:filter(
                fun(X) -> X > 2 end,
                [1, 2, 3, 4, 5]
            )
        )
    ).

% Or with list comprehensions (more idiomatic)
result_comprehension() ->
    Sum = fun(List) -> lists:sum(List) end,
    Sum([X * 2 || X <- [1, 2, 3, 4, 5], X > 2]).

Why this translation:

  • Elm's pipeline |> → nested function calls or list comprehensions
  • List comprehensions are more idiomatic in Erlang for filter+map
  • lists: module provides functional primitives

Pattern 4: Record Updates

Elm:

type alias User = { name : String, age : Int, email : String }

user : User
user = { name = "Alice", age = 30, email = "[email protected]" }

updatedUser : User
updatedUser = { user | age = 31 }

Erlang:

-record(user, {
    name :: binary(),
    age :: integer(),
    email :: binary()
}).

user() ->
    #user{name = <<"Alice">>, age = 30, email = <<"[email protected]">>}.

updated_user() ->
    User = user(),
    User#user{age = 31}.

% Or with maps
user_map() ->
    #{name => <<"Alice">>, age => 30, email => <<"[email protected]">>}.

updated_user_map() ->
    User = user_map(),
    User#{age := 31}.  % := for updating existing key

Why this translation:

  • Elm records → Erlang records (compile-time) or maps (runtime)
  • Records provide type checking with dialyzer
  • Maps are more flexible but less type-safe

Pattern 5: Cmd → Process Operations

Elm:

type Msg
    = GotUsers (Result Http.Error (List User))

getUsers : Cmd Msg
getUsers =
    Http.get
        { url = "https://api.example.com/​users"
        , expect = Http.expectJson GotUsers usersDecoder
        }

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FetchUsers ->
            ( { model | loading = True }, getUsers )

        GotUsers result ->
            case result of
                Ok users -> ( { model | users = users, loading = False }, Cmd.none )
                Err error -> ( { model | error = Just error, loading = False }, Cmd.none )

Erlang:

-module(user_fetcher).
-behaviour(gen_server).

handle_cast(fetch_users, State) ->
    % Spawn async HTTP request
    Self = self(),
    spawn(fun() ->
        case httpc:request(get, {"https://api.example.com/​users", []}, [], []) of
            {ok, {{_, 200, _}, _, Body}} ->
                Users = decode_users(Body),
                Self ! {users_fetched, {ok, Users}};
            {error, Reason} ->
                Self ! {users_fetched, {error, Reason}}
        end
    end),
    {noreply, State#{loading => true}}.

handle_info({users_fetched, {ok, Users}}, State) ->
    {noreply, State#{users => Users, loading => false, error => undefined}};
handle_info({users_fetched, {error, Reason}}, State) ->
    {noreply, State#{error => Reason, loading => false}}.

Why this translation:

  • Elm's Cmd → spawn/​spawn_link + message passing
  • HTTP in Elm runtime → explicit httpc or third-party libraries (hackney, gun)
  • Elm's managed effects → Erlang's explicit process control
  • Error handling moves from Result type to message patterns

Pattern 6: Sub → Timers and Receives

Elm:

subscriptions : Model -> Sub Msg
subscriptions model =
    Time.every 1000 Tick  -- Every second

type Msg
    = Tick Time.Posix

Erlang:

init([]) ->
    % Set up recurring timer
    {ok, TRef} = timer:send_interval(1000, self(), tick),
    {ok, #{timer_ref => TRef}}.

handle_info(tick, State) ->
    % Handle timer tick
    NewState = do_periodic_work(State),
    {noreply, NewState};
handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, #{timer_ref := TRef}) ->
    timer:cancel(TRef),
    ok.

Why this translation:

  • Elm's Time.every → timer:send_interval
  • Sub message → handle_info callback
  • Cleanup handled in terminate/​2

Error Handling

Elm's Guarantees → Erlang's Let-It-Crash

Elm Approach (No Runtime Errors):

-- Elm: Everything must be handled at compile time
parseAge : String -> Result String Int
parseAge str =
    case String.toInt str of
        Just age ->
            if age >= 0 then
                Ok age
            else
                Err "Age must be non-negative"
        Nothing ->
            Err "Not a valid number"

Erlang Approach (Let It Crash):

% Erlang: Let supervisor restart on invalid input
parse_age(Str) ->
    Age = list_to_integer(Str),  % Crashes on invalid input
    true = Age >= 0,              % Crashes if negative
    Age.

% Or with explicit error handling when needed
parse_age_safe(Str) ->
    try list_to_integer(Str) of
        Age when Age >= 0 -> {ok, Age};
        Age -> {error, {negative_age, Age}}
    catch
        error:badarg -> {error, not_a_number}
    end.

Translation Strategy:

  • Elm's Result → {ok, Value} / {error, Reason} for APIs
  • Elm's Maybe → {ok, Value} / undefined / {error, not_found}
  • Critical paths: explicit error tuples
  • Internal functions: let it crash, supervisor restarts
  • Supervision tree ensures fault tolerance

Elm Result Chains → Erlang Error Tuples

Elm:

processUser : String -> Result String ProcessedUser
processUser input =
    parseJson input
        |> Result.andThen validateUser
        |> Result.andThen enrichUser
        |> Result.map processUser

Erlang:

process_user(Input) ->
    case parse_json(Input) of
        {ok, Json} ->
            case validate_user(Json) of
                {ok, User} ->
                    case enrich_user(User) of
                        {ok, Enriched} ->
                            {ok, process_user_data(Enriched)};
                        {error, Reason} -> {error, Reason}
                    end;
                {error, Reason} -> {error, Reason}
            end;
        {error, Reason} -> {error, Reason}
    end.

% Or with helper for chaining
chain(Value, []) -> {ok, Value};
chain({ok, Value}, [F | Rest]) ->
    chain(F(Value), Rest);
chain({error, Reason}, _) ->
    {error, Reason}.

process_user_chain(Input) ->
    chain(parse_json(Input), [
        fun validate_user/​1,
        fun enrich_user/​1,
        fun(U) -> {ok, process_user_data(U)} end
    ]).

Concurrency Patterns

Elm Managed Effects → Erlang Explicit Processes

Conceptual Mapping:

Elm ConceptErlang EquivalentNotes
Elm Runtime manages concurrencyYou spawn and manage processesExplicit control
Cmd.batchMultiple spawn/​cast callsParallel operations
Cmd.noneDon't spawn, synchronousStay in current process
update guaranteed sequentialgen_server callbacks sequentialOTP guarantees
Multiple SubsMultiple receive patternsPattern match messages

Example: Concurrent HTTP Requests

Elm:

-- Runtime handles concurrency automatically
update msg model =
    case msg of
        FetchAll ->
            ( { model | loading = True }
            , Cmd.batch
                [ Http.get { url = "/​api/​users", expect = Http.expectJson GotUsers decoder }
                , Http.get { url = "/​api/​posts", expect = Http.expectJson GotPosts decoder }
                , Http.get { url = "/​api/​comments", expect = Http.expectJson GotComments decoder }
                ]
            )

Erlang:

handle_cast(fetch_all, State) ->
    Self = self(),
    % Spawn three concurrent requests
    spawn_link(fun() -> fetch_and_send(Self, "/​api/​users", users) end),
    spawn_link(fun() -> fetch_and_send(Self, "/​api/​posts", posts) end),
    spawn_link(fun() -> fetch_and_send(Self, "/​api/​comments", comments) end),
    {noreply, State#{loading => true, pending => 3}}.

fetch_and_send(Parent, Url, Type) ->
    Result = httpc:request(get, {Url, []}, [], []),
    Parent ! {fetched, Type, Result}.

handle_info({fetched, Type, Result}, State = #{pending := Pending}) ->
    NewState = State#{Type => Result, pending => Pending - 1},
    case maps:get(pending, NewState) of
        0 -> {noreply, NewState#{loading => false}};
        _ -> {noreply, NewState}
    end.

Architecture Translation

TEA → OTP Application

Elm Application Structure:

src/
  Main.elm          -- Entry point (Browser.element)
  Types.elm         -- Shared types (Model, Msg)
  Api.elm           -- HTTP functions
  Page/
    Home.elm        -- Page modules

Erlang/​OTP Application Structure:

src/
  myapp.app.src        -- Application metadata
  myapp_app.erl        -- Application behavior (entry point)
  myapp_sup.erl        -- Top-level supervisor
  myapp_server.erl     -- Main gen_server (Model + update)
  myapp_api.erl        -- HTTP client functions
  myapp_worker.erl     -- Worker processes

Supervision Tree

Elm: Single runtime, no crashes Erlang: Supervision tree for fault tolerance

-module(myapp_sup).
-behaviour(supervisor).

init([]) ->
    SupFlags = #{
        strategy => one_for_one,
        intensity => 5,
        period => 60
    },
    ChildSpecs = [
        #{
            id => main_server,
            start => {myapp_server, start_link, []},
            restart => permanent,
            shutdown => 5000,
            type => worker
        },
        #{
            id => worker_pool_sup,
            start => {myapp_worker_sup, start_link, []},
            restart => permanent,
            shutdown => infinity,
            type => supervisor
        }
    ],
    {ok, {SupFlags, ChildSpecs}}.

JSON Handling

Elm Decoders → Erlang Parsing

Elm:

import Json.Decode as Decode exposing (Decoder)

type alias User = { name : String, age : Int, email : String }

userDecoder : Decoder User
userDecoder =
    Decode.map3 User
        (Decode.field "name" Decode.string)
        (Decode.field "age" Decode.int)
        (Decode.field "email" Decode.string)

Erlang:

-record(user, {
    name :: binary(),
    age :: integer(),
    email :: binary()
}).

% Using jsone library
decode_user(Json) ->
    case jsone:decode(Json, [{object_format, map}]) of
        #{<<"name">> := Name, <<"age">> := Age, <<"email">> := Email} ->
            {ok, #user{name = Name, age = Age, email = Email}};
        _ ->
            {error, invalid_json}
    end.

% Or with jiffy
decode_user_jiffy(Json) ->
    {Props} = jiffy:decode(Json),
    Name = proplists:get_value(<<"name">>, Props),
    Age = proplists:get_value(<<"age">>, Props),
    Email = proplists:get_value(<<"email">>, Props),
    #user{name = Name, age = Age, email = Email}.

Elm Encoders → Erlang Encoding:

Elm:

import Json.Encode as Encode

encodeUser : User -> Encode.Value
encodeUser user =
    Encode.object
        [ ( "name", Encode.string user.name )
        , ( "age", Encode.int user.age )
        , ( "email", Encode.string user.email )
        ]

Erlang:

encode_user(#user{name = Name, age = Age, email = Email}) ->
    jsone:encode(#{
        <<"name">> => Name,
        <<"age">> => Age,
        <<"email">> => Email
    }).

Common Pitfalls

1. Assuming Compile-Time Safety

Problem: Elm catches all errors at compile time; Erlang relies on runtime checks and supervision.

Elm:

-- Compiler forces you to handle all cases
processResult : Result Error Value -> String
processResult result =
    case result of
        Ok value -> "Success"
        Err error -> "Failed"  -- MUST handle or won't compile

Erlang:

% No compile-time exhaustiveness checking
process_result({ok, _Value}) -> "Success".
% Forgot {error, _} case → runtime crash (but supervisor restarts)

Solution: Use dialyzer for static analysis, embrace let-it-crash philosophy with supervision.

2. Misunderstanding String Types

Problem: Elm's String is always Unicode text; Erlang has both binaries and lists.

Bad:

% Mixing strings and binaries
Name = "Alice",  % List of integers
Email = <<"[email protected]">>,  % Binary
Combined = Name ++ Email.  % ERROR: can't concatenate list and binary

Good:

% Be consistent: use binaries for text
Name = <<"Alice">>,
Email = <<"[email protected]">>,
Combined = <<Name/​binary, <<" - ">>/​binary, Email/​binary>>.

3. Over-Using Try-Catch

Problem: Translating Elm's explicit error handling to defensive try-catch everywhere.

Bad:

% Over-defensive (not idiomatic Erlang)
process_data(Data) ->
    try
        Step1 = validate(Data),
        Step2 = transform(Step1),
        Step3 = save(Step2),
        {ok, Step3}
    catch
        _:_ -> {error, something_failed}
    end.

Good:

% Let it crash in workers, handle errors at API boundaries
process_data(Data) ->
    Step1 = validate(Data),    % Crash if invalid
    Step2 = transform(Step1),  % Crash if transform fails
    save(Step2).               % Crash if save fails
    % Supervisor will restart this process if it crashes

4. Not Using OTP Behaviors

Problem: Writing raw process loops instead of using gen_server, gen_statem.

Bad:

% Reimplementing gen_server
loop(State) ->
    receive
        {From, get} ->
            From ! {self(), State},
            loop(State);
        {From, set, NewState} ->
            From ! {self(), ok},
            loop(NewState)
    end.

Good:

% Use OTP behaviors
-behaviour(gen_server).

handle_call(get, _From, State) ->
    {reply, State, State};
handle_call({set, NewState}, _From, _State) ->
    {reply, ok, NewState}.

5. Forgetting Binary Pattern Matching

Problem: Not using Erlang's powerful binary pattern matching for parsing.

Elm (can't do this):

-- Must use String functions
parseHeader : String -> Maybe Header

Erlang (idiomatic):

% Binary pattern matching is idiomatic
parse_header(<<Type:8, Length:16, Rest/​binary>>) ->
    <<Payload:Length/​binary, Remaining/​binary>> = Rest,
    {ok, {Type, Payload}, Remaining}.

Tooling

ToolPurposeNotes
rebar3Build toolLike Elm's build system
dialyzerStatic analysisCatches type errors
elvisStyle checkerCode linting
xrefCross-referenceFind unused functions
eunitUnit testingLike elm-test
common_testIntegration testingFull test suites
properProperty-based testingLike elm-explorations/​test
jsone / jiffyJSON parsingExternal libraries
hackney / gunHTTP clientLike elm/​http

Examples

Example 1: Simple - Counter

Elm:

type alias Model = { count : Int }

type Msg = Increment | Decrement | Reset

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Increment -> ( { model | count = model.count + 1 }, Cmd.none )
        Decrement -> ( { model | count = model.count - 1 }, Cmd.none )
        Reset -> ( { count = 0 }, Cmd.none )

Erlang:

-module(counter).
-behaviour(gen_server).
-export([start_link/​0, increment/​0, decrement/​0, reset/​0, get/​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, [], []).

increment() -> gen_server:cast(?MODULE, increment).
decrement() -> gen_server:cast(?MODULE, decrement).
reset() -> gen_server:cast(?MODULE, reset).
get() -> gen_server:call(?MODULE, get).

init([]) ->
    {ok, 0}.

handle_call(get, _From, Count) ->
    {reply, Count, Count}.

handle_cast(increment, Count) ->
    {noreply, Count + 1};
handle_cast(decrement, Count) ->
    {noreply, Count - 1};
handle_cast(reset, _Count) ->
    {noreply, 0}.

handle_info(_Info, State) -> {noreply, State}.
terminate(_Reason, _State) -> ok.
code_change(_Old, State, _Extra) -> {ok, State}.

Example 2: Medium - User Management

Elm:

type alias User = { id : Int, name : String, email : String }

type Msg
    = FetchUsers
    | GotUsers (Result Http.Error (List User))
    | DeleteUser Int
    | UserDeleted (Result Http.Error ())

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FetchUsers ->
            ( { model | loading = True }, fetchUsers )

        GotUsers result ->
            case result of
                Ok users -> ( { model | users = users, loading = False }, Cmd.none )
                Err error -> ( { model | error = Just error, loading = False }, Cmd.none )

        DeleteUser id ->
            ( model, deleteUser id )

        UserDeleted result ->
            case result of
                Ok _ -> ( model, fetchUsers )
                Err error -> ( { model | error = Just error }, Cmd.none )

Erlang:

-module(user_manager).
-behaviour(gen_server).

-record(state, {
    users = [] :: [map()],
    loading = false :: boolean(),
    error = undefined :: undefined | binary()
}).

%% API
-export([start_link/​0, fetch_users/​0, delete_user/​1]).
-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, [], []).

fetch_users() ->
    gen_server:cast(?MODULE, fetch_users).

delete_user(Id) ->
    gen_server:cast(?MODULE, {delete_user, Id}).

init([]) ->
    {ok, #state{}}.

handle_cast(fetch_users, State) ->
    Self = self(),
    spawn_link(fun() ->
        Result = fetch_users_http(),
        Self ! {users_fetched, Result}
    end),
    {noreply, State#state{loading = true}};

handle_cast({delete_user, Id}, State) ->
    Self = self(),
    spawn_link(fun() ->
        Result = delete_user_http(Id),
        Self ! {user_deleted, Result}
    end),
    {noreply, State}.

handle_info({users_fetched, {ok, Users}}, State) ->
    {noreply, State#state{users = Users, loading = false, error = undefined}};
handle_info({users_fetched, {error, Reason}}, State) ->
    {noreply, State#state{loading = false, error = format_error(Reason)}};

handle_info({user_deleted, {ok, _}}, State) ->
    % Refetch users after deletion
    gen_server:cast(self(), fetch_users),
    {noreply, State};
handle_info({user_deleted, {error, Reason}}, State) ->
    {noreply, State#state{error = format_error(Reason)}}.

handle_call(_Request, _From, State) ->
    {reply, ok, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%% Internal functions
fetch_users_http() ->
    case httpc:request(get, {"https://api.example.com/​users", []}, [], []) of
        {ok, {{_, 200, _}, _, Body}} ->
            {ok, jsone:decode(Body, [{object_format, map}])};
        {error, Reason} ->
            {error, Reason}
    end.

delete_user_http(Id) ->
    Url = "https://api.example.com/​users/" ++ integer_to_list(Id),
    case httpc:request(delete, {Url, []}, [], []) of
        {ok, {{_, 204, _}, _, _}} ->
            {ok, deleted};
        {error, Reason} ->
            {error, Reason}
    end.

format_error(Reason) ->
    list_to_binary(io_lib:format("~p", [Reason])).

Example 3: Complex - State Machine with Timers

Elm:

type State
    = Idle
    | Running { startTime : Time.Posix, elapsed : Float }
    | Paused { elapsed : Float }

type Msg
    = Start
    | Stop
    | Pause
    | Tick Time.Posix

update : Msg -> State -> ( State, Cmd Msg )
update msg state =
    case ( msg, state ) of
        ( Start, Idle ) ->
            ( Running { startTime = Time.millisToPosix 0, elapsed = 0 }, Cmd.none )

        ( Stop, Running _ ) ->
            ( Idle, Cmd.none )

        ( Stop, Paused _ ) ->
            ( Idle, Cmd.none )

        ( Pause, Running { elapsed } ) ->
            ( Paused { elapsed = elapsed }, Cmd.none )

        ( Start, Paused { elapsed } ) ->
            ( Running { startTime = Time.millisToPosix 0, elapsed = elapsed }, Cmd.none )

        ( Tick now, Running { startTime, elapsed } ) ->
            let
                delta = Time.posixToMillis now - Time.posixToMillis startTime
            in
            ( Running { startTime = now, elapsed = elapsed + toFloat delta / 1000 }, Cmd.none )

        _ ->
            ( state, Cmd.none )

subscriptions : State -> Sub Msg
subscriptions state =
    case state of
        Running _ ->
            Time.every 100 Tick

        _ ->
            Sub.none

Erlang:

-module(stopwatch).
-behaviour(gen_statem).

-export([start_link/​0, start/​0, stop/​0, pause/​0, get_elapsed/​0]).
-export([init/​1, callback_mode/​0, idle/​3, running/​3, paused/​3, terminate/​3]).

-record(data, {
    start_time = undefined :: undefined | integer(),
    elapsed = 0 :: float(),
    timer_ref = undefined :: undefined | reference()
}).

%% API
start_link() ->
    gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []).

start() -> gen_statem:cast(?MODULE, start).
stop() -> gen_statem:cast(?MODULE, stop).
pause() -> gen_statem:cast(?MODULE, pause).
get_elapsed() -> gen_statem:call(?MODULE, get_elapsed).

%% Callbacks
callback_mode() -> state_functions.

init([]) ->
    {ok, idle, #data{}}.

%% State: idle
idle(cast, start, Data) ->
    {ok, TRef} = timer:send_interval(100, tick),
    {next_state, running, Data#data{
        start_time = erlang:monotonic_time(millisecond),
        elapsed = 0,
        timer_ref = TRef
    }};
idle({call, From}, get_elapsed, Data) ->
    {keep_state, Data, [{reply, From, 0}]};
idle(_EventType, _Event, Data) ->
    {keep_state, Data}.

%% State: running
running(cast, stop, Data = #data{timer_ref = TRef}) ->
    timer:cancel(TRef),
    {next_state, idle, #data{}};
running(cast, pause, Data = #data{timer_ref = TRef, elapsed = Elapsed}) ->
    timer:cancel(TRef),
    {next_state, paused, Data#data{timer_ref = undefined}};
running(info, tick, Data = #data{start_time = StartTime, elapsed = Elapsed}) ->
    Now = erlang:monotonic_time(millisecond),
    Delta = (Now - StartTime) / 1000.0,
    {keep_state, Data#data{
        start_time = Now,
        elapsed = Elapsed + Delta
    }};
running({call, From}, get_elapsed, Data = #data{elapsed = Elapsed}) ->
    {keep_state, Data, [{reply, From, Elapsed}]};
running(_EventType, _Event, Data) ->
    {keep_state, Data}.

%% State: paused
paused(cast, start, Data = #data{elapsed = Elapsed}) ->
    {ok, TRef} = timer:send_interval(100, tick),
    {next_state, running, Data#data{
        start_time = erlang:monotonic_time(millisecond),
        timer_ref = TRef
    }};
paused(cast, stop, _Data) ->
    {next_state, idle, #data{}};
paused({call, From}, get_elapsed, Data = #data{elapsed = Elapsed}) ->
    {keep_state, Data, [{reply, From, Elapsed}]};
paused(_EventType, _Event, Data) ->
    {keep_state, Data}.

terminate(_Reason, _State, #data{timer_ref = TRef}) when TRef =/= undefined ->
    timer:cancel(TRef),
    ok;
terminate(_Reason, _State, _Data) ->
    ok.

See Also

  • meta-convert-dev - Foundational conversion patterns with cross-language examples
  • lang-elm-dev - Elm development patterns and The Elm Architecture
  • lang-erlang-dev - Erlang/​OTP fundamentals, processes, and behaviors
  • patterns-concurrency-dev - Concurrency patterns across languages (Cmd/​Sub vs processes)
  • patterns-serialization-dev - JSON handling across languages (decoders vs parsing)
  • patterns-metaprogramming-dev - Compile-time vs runtime code generation

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