Skip to content
Back to skills

Convert Elm Erlang

ASecurity

Bidirectional conversion between Elm and Erlang. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Elm↔Erlang specific patterns.

  • 8 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 6, 2026
developmentgotestingapifrontendbackend

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 6, 2026

npx -y skills add aRustyDev/agents --skill convert-elm-erlang --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Convert Elm Erlang?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Convert Elm Erlang
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/arustydev-convert-elm-erlang-agents/badge)](https://www.skillsdirectory.com/skills/arustydev-convert-elm-erlang-agents)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: convert-elm-erlang
description: Bidirectional conversion between Elm and Erlang. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Elm↔Erlang specific patterns.
---

# Elm ↔ Erlang Conversion

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`

---

## 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 = <<Name/binary, <<" - ">>/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(<<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:**
```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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…