Claude-skill-registry 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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convert-elm-erlang" ~/.claude/skills/majiayu000-claude-skill-registry-convert-elm-erlang && rm -rf "$T"
skills/data/convert-elm-erlang/SKILL.mdConvert 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
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
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 |
|---|---|---|
| or | Use binaries for efficiency |
| | Direct mapping |
| | Direct mapping |
| / | Atoms in Erlang |
| | Direct mapping |
| / / | Tagged tuples |
| / | Standard Erlang convention |
| or | Records for structure |
| Message patterns | Process mailbox messages |
| state | OTP state management |
function | | OTP callbacks |
| / / | Process operations |
| / 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 |
|---|---|---|
| | UTF-8 binary: |
| | List of codepoints: (less efficient) |
| | Arbitrary precision |
| | IEEE 754 double precision |
| / | Atoms, not a separate type |
| | Unicode codepoint |
(unit) | / | Atom for no value |
Collection Types
| Elm | Erlang | Notes |
|---|---|---|
| | Linked list |
| | Fixed-size arrays (rare) |
| | Modern maps (Erlang 17+) |
| | Legacy dict module |
| | Set data structure |
| | Tuple (fixed size) |
| | Tuple with 3+ elements |
Composite Types
| Elm | Erlang | Notes |
|---|---|---|
| | Record with type specs |
| | Map (more flexible) |
| Message patterns in receive | Patterns, not types |
| | Tagged tuples |
| | 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
or{error, Reason}undefined
→Just value{ok, Value}- Erlang convention:
for success,{ok, Result}
for failure{error, Reason} - 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
module provides functional primitiveslists:
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}
for APIs{error, Reason} - 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 |
|---|---|---|
| Build tool | Like Elm's build system |
| Static analysis | Catches type errors |
| Style checker | Code linting |
| Cross-reference | Find unused functions |
| Unit testing | Like elm-test |
| Integration testing | Full test suites |
| Property-based testing | Like elm-explorations/test |
/ | JSON parsing | External libraries |
/ | 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
- Foundational conversion patterns with cross-language examplesmeta-convert-dev
- Elm development patterns and The Elm Architecturelang-elm-dev
- Erlang/OTP fundamentals, processes, and behaviorslang-erlang-dev
- Concurrency patterns across languages (Cmd/Sub vs processes)patterns-concurrency-dev
- JSON handling across languages (decoders vs parsing)patterns-serialization-dev
- Compile-time vs runtime code generationpatterns-metaprogramming-dev