Claude Code Plugins

Community-maintained marketplace

Feedback

convert-elm-erlang

@aRustyDev/ai
0
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name convert-elm-erlang
description 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.

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

Elm Erlang Notes
String binary() or list() Use binaries for efficiency
Int integer() Direct mapping
Float float() Direct mapping
Bool true / false Atoms in Erlang
List a [a] Direct mapping
Maybe a {ok, a} / undefined / error Tagged tuples
Result e a {ok, a} / {error, e} Standard Erlang convention
type alias -record() or map() Records for structure
type Msg Message patterns Process mailbox messages
Model gen_server state OTP state management
update function handle_call/cast OTP callbacks
Cmd Msg spawn / ! / gen_server:call Process operations
Sub Msg receive / timers Message 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

Elm Erlang Notes
String binary() UTF-8 binary: <<"Hello">>
String string() List of codepoints: "Hello" (less efficient)
Int integer() Arbitrary precision
Float float() IEEE 754 double precision
Bool true / false Atoms, not a separate type
Char integer() Unicode codepoint
() (unit) ok / undefined Atom for no value

Collection Types

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

Composite Types

Elm Erlang Notes
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 | Decrement Message patterns in receive Patterns, 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 = "alice@example.com" }

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

Erlang:

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

user() ->
    #user{name = <<"Alice">>, age = 30, email = <<"alice@example.com">>}.

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

% Or with maps
user_map() ->
    #{name => <<"Alice">>, age => 30, email => <<"alice@example.com">>}.

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 Concept Erlang Equivalent Notes
Elm Runtime manages concurrency You spawn and manage processes Explicit control
Cmd.batch Multiple spawn/cast calls Parallel operations
Cmd.none Don't spawn, synchronous Stay in current process
update guaranteed sequential gen_server callbacks sequential OTP guarantees
Multiple Subs Multiple receive patterns Pattern 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 = <<"alice@example.com">>,  % Binary
Combined = Name ++ Email.  % ERROR: can't concatenate list and binary

Good:

% Be consistent: use binaries for text
Name = <<"Alice">>,
Email = <<"alice@example.com">>,
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

Tool Purpose Notes
rebar3 Build tool Like Elm's build system
dialyzer Static analysis Catches type errors
elvis Style checker Code linting
xref Cross-reference Find unused functions
eunit Unit testing Like elm-test
common_test Integration testing Full test suites
proper Property-based testing Like elm-explorations/test
jsone / jiffy JSON parsing External libraries
hackney / gun HTTP client Like 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