AgentSkillsCN

convert-erlang-elm

将 Erlang 代码转换为符合 Elm 风格的代码。当需要将 Erlang 后端逻辑迁移到 Elm 前端应用、将 BEAM VM 的编程模式转化为函数式前端代码,或重构分布式系统以打造类型安全的用户界面时,可使用此技能。该技能在 meta-convert-dev 的基础上,新增了专属于 Erlang 到 Elm 的转换模式。

SKILL.md
--- frontmatter
name: convert-erlang-elm
description: 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.

Convert 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

  • 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: 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

ErlangElmNotes
atom()String or custom typeAtoms become string literals or union types
binary()StringUTF-8 encoded strings
integer()IntArbitrary precision → fixed size
float()FloatDirect mapping
boolean()Booltrue/false mapping
list()List aHomogeneous typed lists
tuple()Custom type or recordNamed fields preferred
map()Dict k vKey-value storage
pid()N/ANo direct equivalent (use Cmd/Sub)
undefinedNothing in Maybe aExplicit nullability
{ok, Value}Just Value or Ok ValueSuccess wrapper
{error, Reason}Err Reason in Result e aError wrapper

Architectural Paradigm Shift

From OTP to The Elm Architecture (TEA)

AspectErlang OTPElm TEA
PurposeDistributed, fault-tolerant backendType-safe, reactive frontend
ConcurrencyMillions of processesSingle-threaded event loop
StateProcess-local mutable stateImmutable application state
CommunicationMessage passing between processesCommands/Subscriptions to runtime
Error handlingLet-it-crash + supervision treesCompiler-enforced exhaustive handling

Mapping OTP Behaviors to TEA Components

Erlang gen_server:

erlang
-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:

elm
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

ErlangElmNotes
true / falseTrue / FalseCapitalized in Elm
4242Integer literals
3.143.14Float literals
<<"binary">>"String"UTF-8 strings
'atom'"string" or custom typeContext-dependent

Collection Types

ErlangElmExample
[1, 2, 3][1, 2, 3]Homogeneous lists
#{key => value}Dict.fromList [("key", value)]Requires Dict module
{ok, 42}Ok 42Result type
{error, "failed"}Err "failed"Result type
undefinedNothingMaybe type

Structured Types

Erlang Records → Elm Type Aliases

erlang
%% Erlang
-record(user, {
    id :: integer(),
    name :: binary(),
    age :: integer() | undefined
}).
elm
-- Elm
type alias User =
    { id : Int
    , name : String
    , age : Maybe Int
    }

Idiom Translation

1. Pattern Matching

Erlang:

erlang
classify(N) when N > 0 -> positive;
classify(N) when N < 0 -> negative;
classify(0) -> zero.

Elm:

elm
classify : Int -> String
classify n =
    case compare n 0 of
        GT -> "positive"
        LT -> "negative"
        EQ -> "zero"

2. List Processing

Erlang:

erlang
Squares = [X * X || X <- [1, 2, 3, 4, 5], X rem 2 == 0].

Elm:

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:

erlang
parse_int(Str) ->
    try binary_to_integer(Str) of
        Int -> {ok, Int}
    catch
        error:badarg -> {error, invalid_integer}
    end.

Elm:

elm
parseInt : String -> Result String Int
parseInt str =
    String.toInt str
        |> Result.fromMaybe "Invalid integer"

4. Optional Values

Erlang:

erlang
get_timeout(#config{timeout = undefined}) -> 5000;
get_timeout(#config{timeout = T}) -> T.

Elm:

elm
getTimeout : Config -> Int
getTimeout config =
    Maybe.withDefault 5000 config.timeout

5. HTTP Requests (Message Passing Replacement)

Erlang:

erlang
fetch_data(Url) ->
    Pid = self(),
    spawn(fun() ->
        Response = httpc:request(get, {Url, []}, [], []),
        Pid ! {http_response, Response}
    end).

Elm:

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:

erlang
locked(cast, {button, Code}, #{code := Code} = Data) ->
    {next_state, unlocked, Data};
locked(cast, {button, _}, Data) ->
    {keep_state, Data}.

Elm:

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

ErlangElm
Let-it-crash: Supervisors restart failed processesPrevent-all-crashes: Compiler enforces handling all cases
Runtime errors are acceptableCompile-time guarantees eliminate runtime errors

Practical Translation

Erlang:

erlang
safe_divide(_, 0) -> {error, division_by_zero};
safe_divide(X, Y) -> {ok, X / Y}.

Elm:

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

code
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

ErlangElmPurpose
rebar3 compileelm makeBuild project
rebar3 eunitelm-testRun tests
rebar3 shellelm replInteractive shell
dialyzerelm compilerType checking
observerElm debuggerRuntime inspection

Example: Counter with Backend

Elm Frontend:

elm
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

  • lang-erlang-dev - Erlang language fundamentals
  • lang-elm-dev - Elm language fundamentals
  • meta-convert-dev - General conversion methodology
  • convert-elm-erlang - Reverse conversion