Claude-skill-registry convert-erlang-elm
Convert Erlang code to idiomatic Elm. Use when migrating Erlang backend logic to Elm frontend applications, translating BEAM VM patterns to functional frontend code, or refactoring distributed systems to type-safe UIs. Extends meta-convert-dev with Erlang-to-Elm 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-erlang-elm" ~/.claude/skills/majiayu000-claude-skill-registry-convert-erlang-elm && rm -rf "$T"
skills/data/convert-erlang-elm/SKILL.mdConvert Erlang to Elm
Convert Erlang code to idiomatic Elm. This skill extends
meta-convert-dev with Erlang-to-Elm specific type mappings, idiom translations, and architectural patterns for moving from distributed backend systems to type-safe frontend applications.
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: Erlang types → Elm types
- Idiom translations: Erlang patterns → idiomatic Elm
- Architecture patterns: OTP behaviors → The Elm Architecture (TEA)
- Message passing: Process mailboxes → Elm commands/subscriptions
- Error handling: let-it-crash → Maybe/Result types
- Concurrency: Processes/gen_server → Elm runtime effects
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Erlang language fundamentals - see
lang-erlang-dev - Elm language fundamentals - see
lang-elm-dev - Reverse conversion (Elm → Erlang) - see
convert-elm-erlang - Backend-to-backend conversions - see other conversion skills
Quick Reference
| Erlang | Elm | Notes |
|---|---|---|
| or custom type | Atoms become string literals or union types |
| | UTF-8 encoded strings |
| | Arbitrary precision → fixed size |
| | Direct mapping |
| | / mapping |
| | Homogeneous typed lists |
| Custom type or record | Named fields preferred |
| | Key-value storage |
| N/A | No direct equivalent (use Cmd/Sub) |
| in | Explicit nullability |
| or | Success wrapper |
| in | Error wrapper |
Architectural Paradigm Shift
From OTP to The Elm Architecture (TEA)
| Aspect | Erlang OTP | Elm TEA |
|---|---|---|
| Purpose | Distributed, fault-tolerant backend | Type-safe, reactive frontend |
| Concurrency | Millions of processes | Single-threaded event loop |
| State | Process-local mutable state | Immutable application state |
| Communication | Message passing between processes | Commands/Subscriptions to runtime |
| Error handling | Let-it-crash + supervision trees | Compiler-enforced exhaustive handling |
Mapping OTP Behaviors to TEA Components
Erlang gen_server:
-module(counter_server). -behaviour(gen_server). -record(state, {count = 0}). init([]) -> {ok, #state{}}. handle_call(get, _From, State) -> {reply, State#state.count, State}; handle_call({increment, N}, _From, State) -> NewCount = State#state.count + N, {reply, NewCount, State#state{count = NewCount}}.
Elm equivalent using TEA:
module Counter exposing (Model, Msg, init, update, view) -- MODEL type alias Model = { count : Int } init : Model init = { count = 0 } -- UPDATE type Msg = Increment Int | Get update : Msg -> Model -> (Model, Cmd Msg) update msg model = case msg of Increment n -> ( { model | count = model.count + n }, Cmd.none ) Get -> ( model, Cmd.none ) -- VIEW view : Model -> Html Msg view model = div [] [ text ("Count: " ++ String.fromInt model.count) , button [ onClick (Increment 1) ] [ text "Increment" ] ]
Type System Mapping
Primitive Types
| Erlang | Elm | Notes |
|---|---|---|
/ | / | Capitalized in Elm |
| | Integer literals |
| | Float literals |
| | UTF-8 strings |
| or custom type | Context-dependent |
Collection Types
| Erlang | Elm | Example |
|---|---|---|
| | Homogeneous lists |
| | Requires Dict module |
| | Result type |
| | Result type |
| | Maybe type |
Structured Types
Erlang Records → Elm Type Aliases
%% Erlang -record(user, { id :: integer(), name :: binary(), age :: integer() | undefined }).
-- Elm type alias User = { id : Int , name : String , age : Maybe Int }
Idiom Translation
1. Pattern Matching
Erlang:
classify(N) when N > 0 -> positive; classify(N) when N < 0 -> negative; classify(0) -> zero.
Elm:
classify : Int -> String classify n = case compare n 0 of GT -> "positive" LT -> "negative" EQ -> "zero"
2. List Processing
Erlang:
Squares = [X * X || X <- [1, 2, 3, 4, 5], X rem 2 == 0].
Elm:
squares : List Int squares = [1, 2, 3, 4, 5] |> List.filter (\x -> modBy 2 x == 0) |> List.map (\x -> x * x)
3. Error Handling
Erlang:
parse_int(Str) -> try binary_to_integer(Str) of Int -> {ok, Int} catch error:badarg -> {error, invalid_integer} end.
Elm:
parseInt : String -> Result String Int parseInt str = String.toInt str |> Result.fromMaybe "Invalid integer"
4. Optional Values
Erlang:
get_timeout(#config{timeout = undefined}) -> 5000; get_timeout(#config{timeout = T}) -> T.
Elm:
getTimeout : Config -> Int getTimeout config = Maybe.withDefault 5000 config.timeout
5. HTTP Requests (Message Passing Replacement)
Erlang:
fetch_data(Url) -> Pid = self(), spawn(fun() -> Response = httpc:request(get, {Url, []}, [], []), Pid ! {http_response, Response} end).
Elm:
type Msg = GotData (Result Http.Error String) fetchData : String -> Cmd Msg fetchData url = Http.get { url = url , expect = Http.expectString GotData }
6. State Machine
Erlang:
locked(cast, {button, Code}, #{code := Code} = Data) -> {next_state, unlocked, Data}; locked(cast, {button, _}, Data) -> {keep_state, Data}.
Elm:
type DoorState = Locked | Unlocked type Msg = ButtonPressed String update : Msg -> Model -> (Model, Cmd Msg) update msg model = case (msg, model.state) of (ButtonPressed code, Locked) -> if code == model.correctCode then ( { model | state = Unlocked }, Cmd.none ) else ( model, Cmd.none ) _ -> ( model, Cmd.none )
Error Handling Philosophy
Philosophy Shift
| Erlang | Elm |
|---|---|
| Let-it-crash: Supervisors restart failed processes | Prevent-all-crashes: Compiler enforces handling all cases |
| Runtime errors are acceptable | Compile-time guarantees eliminate runtime errors |
Practical Translation
Erlang:
safe_divide(_, 0) -> {error, division_by_zero}; safe_divide(X, Y) -> {ok, X / Y}.
Elm:
type DivisionError = DivisionByZero safeDivide : Float -> Float -> Result DivisionError Float safeDivide a b = if b == 0 then Err DivisionByZero else Ok (a / b)
Migration Strategy
What CAN be converted:
- Business logic (calculations, validations)
- Data transformations
- State machines
- Request/response patterns
What CANNOT be converted:
- Process supervision (no equivalent)
- Distributed systems (Elm is frontend-only)
- Hot code reloading
- Low-level concurrency
Architecture Mapping
Erlang OTP Application │ ├── Supervision Tree ──────────> [Remains in Erlang backend] ├── gen_server (State) ────────> Elm Model + Update ├── handle_call/cast ──────────> Msg variants + update cases ├── State transitions ─────────> Model updates └── API endpoints ─────────────> Elm HTTP commands Result: Hybrid architecture - Backend: Erlang OTP (supervision, distributed state) - Frontend: Elm (UI, client state, type-safe interactions) - Communication: HTTP/WebSocket APIs
Common Pitfalls
1. Trying to Port Process Concurrency
Problem: Erlang's concurrency model doesn't translate to Elm. Solution: Re-architect around TEA with commands/subscriptions.
2. Expecting Mutable State
Problem: Erlang processes have mutable state; Elm is purely functional. Solution: Embrace immutability. Return new model versions from update.
3. Over-relying on Dynamic Types
Problem: Erlang's dynamic typing has no direct Elm equivalent. Solution: Use custom types (union types) to model all possibilities.
4. Ignoring JSON Boundaries
Problem: Assuming Erlang terms can be directly used in Elm. Solution: Always create explicit JSON encoders/decoders for API contracts.
Tooling Translation
| Erlang | Elm | Purpose |
|---|---|---|
| | Build project |
| | Run tests |
| | Interactive shell |
| compiler | Type checking |
| Elm debugger | Runtime inspection |
Example: Counter with Backend
Elm Frontend:
module Counter exposing (main) import Browser import Html exposing (..) import Html.Events exposing (onClick) import Http type alias Model = { count : Int, loading : Bool } type Msg = Increment Int | GotCount (Result Http.Error Int) update : Msg -> Model -> (Model, Cmd Msg) update msg model = case msg of Increment n -> ( { model | loading = True } , incrementCount n ) GotCount (Ok count) -> ( { model | count = count, loading = False } , Cmd.none ) GotCount (Err _) -> ( { model | loading = False } , Cmd.none ) incrementCount : Int -> Cmd Msg incrementCount n = Http.post { url = "/api/counter" , body = Http.jsonBody (Encode.object [("increment", Encode.int n)]) , expect = Http.expectJson GotCount countDecoder }
See Also
- Erlang language fundamentalslang-erlang-dev
- Elm language fundamentalslang-elm-dev
- General conversion methodologymeta-convert-dev
- Reverse conversionconvert-elm-erlang