| 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
- Analyze Elm architecture - Understand TEA structure (Model, View, Update, Subscriptions)
- Map to OTP behaviors - Model → gen_server state, Msg → messages, update → handle_call/cast
- Preserve pure functions - Keep business logic pure, wrap in processes
- Adopt OTP patterns - Don't transliterate; use supervision trees and fault tolerance
- Handle guarantees - Elm's no-runtime-errors → Erlang's let-it-crash
- 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}orundefinedJust 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 exampleslang-elm-dev- Elm development patterns and The Elm Architecturelang-erlang-dev- Erlang/OTP fundamentals, processes, and behaviorspatterns-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