--- 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:** ```elm type TrafficLight = Red | Yellow | Green canGo : TrafficLight -> Bool canGo light = case light of Green -> True Yellow -> False Red -> False ``` **Erlang:** ```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:** ```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:** ```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:** ```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:** ```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:** ```elm result : Int result = [1, 2, 3, 4, 5] |> List.filter (\x -> x > 2) |> List.map (\x -> x * 2) |> List.foldl (+) 0 ``` **Erlang:** ```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:** ```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:** ```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:** ```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:** ```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:** ```elm subscriptions : Model -> Sub Msg subscriptions model = Time.every 1000 Tick -- Every second type Msg = Tick Time.Posix ``` **Erlang:** ```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 -- 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 % 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:** ```elm processUser : String -> Result String ProcessedUser processUser input = parseJson input |> Result.andThen validateUser |> Result.andThen enrichUser |> Result.map processUser ``` **Erlang:** ```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:** ```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:** ```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 ```erlang -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:** ```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:** ```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:** ```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:** ```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:** ```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:** ```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:** ```erlang % 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:** ```erlang % Be consistent: use binaries for text Name = <<"Alice">>, Email = <<"alice@example.com">>, Combined = <>/binary, Email/binary>>. ``` ### 3. Over-Using Try-Catch **Problem:** Translating Elm's explicit error handling to defensive try-catch everywhere. **Bad:** ```erlang % 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:** ```erlang % 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:** ```erlang % Reimplementing gen_server loop(State) -> receive {From, get} -> From ! {self(), State}, loop(State); {From, set, NewState} -> From ! {self(), ok}, loop(NewState) end. ``` **Good:** ```erlang % 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):** ```elm -- Must use String functions parseHeader : String -> Maybe Header ``` **Erlang (idiomatic):** ```erlang % Binary pattern matching is idiomatic parse_header(<>) -> <> = 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:** ```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:** ```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:** ```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:** ```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:** ```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:** ```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