Convert Elixir code to idiomatic Elm. Use when migrating server-side Elixir logic to frontend applications, translating BEAM concurrency patterns to The Elm Architecture, or refactoring Elixir codebases for browser-based UI. Extends meta-convert-dev with Elixir-to-Elm specific patterns.
Scanned 2/10/2026
Install via CLI
openskills install aRustyDev/ai---
name: convert-elixir-elm
description: Convert Elixir code to idiomatic Elm. Use when migrating server-side Elixir logic to frontend applications, translating BEAM concurrency patterns to The Elm Architecture, or refactoring Elixir codebases for browser-based UI. Extends meta-convert-dev with Elixir-to-Elm specific patterns.
---
# Convert Elixir to Elm
Convert Elixir code to idiomatic Elm. This skill extends `meta-convert-dev` with Elixir-to-Elm specific type mappings, idiom translations, and architectural guidance for translating server-side BEAM code to client-side functional UI.
## 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**: Elixir dynamic types → Elm static types
- **Idiom translations**: Pattern matching, pipelines, and functional patterns
- **Architecture translation**: GenServer/OTP → The Elm Architecture (TEA)
- **Concurrency translation**: Processes/message passing → Cmd/Sub model
- **Error handling**: `{:ok, _}` / `{:error, _}` → Result type
- **Effect management**: Side effects anywhere → Managed Cmd/Sub
## This Skill Does NOT Cover
- General conversion methodology - see `meta-convert-dev`
- Elixir language fundamentals - see `lang-elixir-dev`
- Elm language fundamentals - see `lang-elm-dev`
- Reverse conversion (Elm → Elixir) - see `convert-elm-elixir`
- Server-side Elixir deployment - focus on logic portable to frontend
- Phoenix-specific patterns - Phoenix LiveView is client-server hybrid
---
## Quick Reference
| Elixir | Elm | Notes |
|--------|-----|-------|
| `String.t()` | `String` | Direct mapping |
| `integer()` | `Int` | Elm Int is fixed-width |
| `float()` | `Float` | Direct mapping |
| `boolean()` | `Bool` | `:true` / `:false` → `True` / `False` |
| `list(a)` | `List a` | Direct mapping |
| `{a, b}` | `(a, b)` | Tuples up to 3 elements in Elm |
| `map()` / `%{}` | `Dict comparable v` | Elm Dict requires comparable keys |
| `{:ok, value}` | `Ok value` | Result type |
| `{:error, reason}` | `Err reason` | Result type |
| `nil` | `Nothing` | Use Maybe type |
| Pattern match | `case ... of` | Both support pattern matching |
| `\|>` pipe | `\|>` pipe | Identical operator |
| `Enum.map/2` | `List.map` | Similar API |
| GenServer state | TEA Model | State management pattern shift |
| `send/receive` | Cmd/Sub | Effects are managed |
## When Converting Code
1. **Identify pure business logic** - Elm runs in browser, not on BEAM VM
2. **Map dynamic types to static** - Add explicit type annotations
3. **Convert processes to TEA** - GenServers become Model-View-Update
4. **Translate effects** - Side effects become Cmd, subscriptions become Sub
5. **Preserve semantics** - Both are functional, immutable languages
6. **Handle compilation errors first** - Elm compiler guides you to correctness
7. **Test with property-based tests** - Both languages support them well
---
## Type System Mapping
### Primitive Types
| Elixir | Elm | Notes |
|--------|-----|-------|
| `integer()` | `Int` | Elm has fixed-width integers (no BigInt) |
| `float()` | `Float` | Direct mapping |
| `boolean()` | `Bool` | `:true` → `True`, `:false` → `False` |
| `String.t()` | `String` | Both are UTF-8 strings |
| `atom()` | Union types | `:ok`, `:error` become custom type variants |
| `nil` | `Nothing` | Use `Maybe a` type |
| `binary()` | - | No direct binary type; use String or custom encoding |
| `charlist()` | `String` | Convert charlists to strings |
### Collection Types
| Elixir | Elm | Notes |
|--------|-----|-------|
| `list(a)` | `List a` | Direct mapping |
| `{a, b}` | `(a, b)` | Tuples identical, max 3 in Elm |
| `{a, b, c}` | `(a, b, c)` | Max tuple size in Elm |
| `map()` / `%{k => v}` | `Dict comparable v` | Elm requires comparable keys |
| `MapSet.t(a)` | `Set comparable` | Elm requires comparable elements |
| `Keyword.t()` | `List (String, a)` | Convert keyword lists to list of tuples |
| `Range.t()` | `List Int` | Use `List.range start end` |
### Composite Types
| Elixir Pattern | Elm Pattern | Notes |
|----------------|-------------|-------|
| `%{name: String.t(), age: integer()}` | `type alias User = { name : String, age : Int }` | Maps → Records |
| `{:ok, value} \| {:error, reason}` | `Result reason value` | Tagged tuples → Result type |
| `value \| nil` | `Maybe value` | Nil → Nothing, value → Just value |
| `@type result :: :ok \| :error` | `type Result = Ok \| Error` | Atoms → Union type variants |
| Struct (`%User{}`) | `type alias User = { ... }` | Structs → Type aliases |
| Protocol implementation | - | No protocols in Elm, use functions on types |
### Function Types
| Elixir | Elm | Notes |
|--------|-----|-------|
| `(a -> b)` | `a -> b` | Function type identical syntax |
| `(a, b -> c)` | `a -> b -> c` | Elm auto-curries, no arity-2 syntax |
| `(() -> a)` | `() -> a` | No-argument functions |
| Anonymous fn | Lambda | Both support anonymous functions |
---
## Idiom Translation
### Pattern 1: Tagged Tuples → Result
**Elixir:**
```elixir
def divide(a, b) when b != 0, do: {:ok, a / b}
def divide(_, 0), do: {:error, :division_by_zero}
case divide(10, 2) do
{:ok, result} -> IO.puts("Result: #{result}")
{:error, reason} -> IO.puts("Error: #{reason}")
end
```
**Elm:**
```elm
divide : Float -> Float -> Result String Float
divide a b =
if b /= 0 then
Ok (a / b)
else
Err "division_by_zero"
-- Usage
case divide 10 2 of
Ok result ->
"Result: " ++ String.fromFloat result
Err reason ->
"Error: " ++ reason
```
**Why this translation:**
- Elixir's `{:ok, value}` / `{:error, reason}` pattern maps directly to Elm's `Result error value`
- Guards in Elixir (`when b != 0`) become if-expressions in Elm
- Atoms like `:division_by_zero` become strings or custom types
- Both use pattern matching in case expressions
### Pattern 2: Nil Handling → Maybe
**Elixir:**
```elixir
def find_user(id) do
users = %{1 => %{name: "Alice"}, 2 => %{name: "Bob"}}
Map.get(users, id)
end
def display_name(user) do
case user do
nil -> "Anonymous"
%{name: name} -> name
end
end
# Pipeline with defaults
name = find_user(1) |> display_name()
```
**Elm:**
```elm
findUser : Int -> Maybe User
findUser id =
let
users =
Dict.fromList
[ ( 1, { name = "Alice" } )
, ( 2, { name = "Bob" } )
]
in
Dict.get id users
displayName : Maybe User -> String
displayName maybeUser =
case maybeUser of
Nothing ->
"Anonymous"
Just user ->
user.name
-- Pipeline with Maybe.map
name : String
name =
findUser 1
|> Maybe.map .name
|> Maybe.withDefault "Anonymous"
```
**Why this translation:**
- Elixir's `nil` becomes Elm's `Nothing`
- Present values become `Just value`
- `Maybe.withDefault` replaces nil coalescing
- Pattern matching translates directly
### Pattern 3: Enum Operations → List Functions
**Elixir:**
```elixir
[1, 2, 3, 4, 5]
|> Enum.filter(&(rem(&1, 2) == 0))
|> Enum.map(&(&1 * 2))
|> Enum.reduce(0, &(&1 + &2))
```
**Elm:**
```elm
[ 1, 2, 3, 4, 5 ]
|> List.filter (\x -> modBy 2 x == 0)
|> List.map (\x -> x * 2)
|> List.foldl (+) 0
```
**Why this translation:**
- `Enum.filter` → `List.filter`
- `Enum.map` → `List.map`
- `Enum.reduce` → `List.foldl` (or `List.foldr`)
- Pipeline operator `|>` is identical
- Capture operator `&()` becomes lambda `\x ->`
- `rem` becomes `modBy` in Elm
### Pattern 4: Pattern Matching Lists
**Elixir:**
```elixir
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)
def first([head | _]), do: {:ok, head}
def first([]), do: {:error, :empty_list}
```
**Elm:**
```elm
sum : List Int -> Int
sum list =
case list of
[] ->
0
head :: tail ->
head + sum tail
first : List a -> Result String a
first list =
case list of
head :: _ ->
Ok head
[] ->
Err "empty_list"
```
**Why this translation:**
- List pattern matching syntax is nearly identical
- `[head | tail]` in Elixir → `head :: tail` in Elm
- Multiple function clauses become case branches
- Guards can become if-expressions inside branches
### Pattern 5: With Statement → Nested Case or Result.andThen
**Elixir:**
```elixir
def create_user(params) do
with {:ok, validated} <- validate_params(params),
{:ok, user} <- insert_user(validated),
{:ok, _email} <- send_welcome_email(user) do
{:ok, user}
else
{:error, reason} -> {:error, reason}
end
end
```
**Elm:**
```elm
-- Option 1: Nested case
createUser : Params -> Result Error User
createUser params =
case validateParams params of
Ok validated ->
case insertUser validated of
Ok user ->
case sendWelcomeEmail user of
Ok _ ->
Ok user
Err reason ->
Err reason
Err reason ->
Err reason
Err reason ->
Err reason
-- Option 2: Result.andThen (preferred)
createUser : Params -> Result Error User
createUser params =
validateParams params
|> Result.andThen insertUser
|> Result.andThen (\user ->
sendWelcomeEmail user
|> Result.map (\_ -> user)
)
```
**Why this translation:**
- Elixir's `with` chains `{:ok, _}` results
- Elm uses `Result.andThen` for equivalent chaining
- Early error returns are handled by Result's short-circuiting
- Pipeline style preserves readability
---
## Architecture Translation: GenServer → The Elm Architecture
### GenServer State Management
**Elixir (GenServer):**
```elixir
defmodule Counter do
use GenServer
# Client API
def start_link(initial_value) do
GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
end
def increment do
GenServer.call(__MODULE__, :increment)
end
def get do
GenServer.call(__MODULE__, :get)
end
# Server Callbacks
@impl true
def init(initial_value), do: {:ok, initial_value}
@impl true
def handle_call(:increment, _from, state) do
{:reply, state + 1, state + 1}
end
def handle_call(:get, _from, state) do
{:reply, state, state}
end
end
```
**Elm (TEA Model):**
```elm
-- MODEL
type alias Model =
{ count : Int
}
init : () -> ( Model, Cmd Msg )
init _ =
( { count = 0 }, Cmd.none )
-- UPDATE
type Msg
= Increment
| Get
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Increment ->
( { model | count = model.count + 1 }, Cmd.none )
Get ->
-- In Elm, view always has access to model
-- No need for explicit "get" message
( model, Cmd.none )
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Increment ] [ text "+" ]
, div [] [ text (String.fromInt model.count) ]
]
```
**Why this translation:**
- GenServer state → Model
- Client API calls → Msg variants
- `handle_call` → `update` function branches
- Synchronous replies → View reads model directly
- No process needed; Elm runtime manages state
### Process Communication → Cmd/Sub
**Elixir (Process Messages):**
```elixir
# Sending process
pid = spawn(fn ->
receive do
{:fetch_user, caller, user_id} ->
user = fetch_user_from_db(user_id)
send(caller, {:user_fetched, user})
end
end)
send(pid, {:fetch_user, self(), 123})
receive do
{:user_fetched, user} -> IO.inspect(user)
after
5000 -> IO.puts("Timeout")
end
```
**Elm (Commands and Subscriptions):**
```elm
-- Commands represent effects to perform
type Msg
= FetchUser Int
| GotUser (Result Http.Error User)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FetchUser userId ->
( { model | loading = True }
, Http.get
{ url = "/api/users/" ++ String.fromInt userId
, expect = Http.expectJson GotUser userDecoder
}
)
GotUser result ->
case result of
Ok user ->
( { model | user = Just user, loading = False }
, Cmd.none
)
Err _ ->
( { model | loading = False }
, Cmd.none
)
-- Subscriptions for incoming events
subscriptions : Model -> Sub Msg
subscriptions model =
-- Listen to time every second
Time.every 1000 Tick
```
**Why this translation:**
- `send` → Create Cmd in update
- `receive` → Handle Msg in update
- Process spawning → Cmd.batch for multiple effects
- Message passing → Msg type with variants
- Timeouts → Handled by Sub/Cmd cancellation
### Supervision Trees → Application Structure
**Elixir (Supervisor):**
```elixir
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
{Counter, 0},
{UserCache, []},
{DatabasePool, pool_size: 10}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
```
**Elm (Module Organization):**
```elm
-- No supervision needed - no crashes!
-- Instead: Organize code into modules
-- Main.elm
module Main exposing (main)
import Browser
import Counter
import UserCache
type alias Model =
{ counter : Counter.Model
, userCache : UserCache.Model
}
type Msg
= CounterMsg Counter.Msg
| UserCacheMsg UserCache.Msg
init : () -> ( Model, Cmd Msg )
init _ =
let
( counterModel, counterCmd ) =
Counter.init ()
( cacheModel, cacheCmd ) =
UserCache.init ()
in
( { counter = counterModel
, userCache = cacheModel
}
, Cmd.batch
[ Cmd.map CounterMsg counterCmd
, Cmd.map UserCacheMsg cacheCmd
]
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
CounterMsg subMsg ->
let
( counterModel, counterCmd ) =
Counter.update subMsg model.counter
in
( { model | counter = counterModel }
, Cmd.map CounterMsg counterCmd
)
UserCacheMsg subMsg ->
let
( cacheModel, cacheCmd ) =
UserCache.update subMsg model.userCache
in
( { model | userCache = cacheModel }
, Cmd.map UserCacheMsg cacheCmd
)
```
**Why this translation:**
- No supervision needed - Elm has no runtime exceptions
- Module composition replaces supervision trees
- Each "child" is a module with its own Model/Msg/update
- Parent routes messages to appropriate child module
- Cmd.map translates between parent and child messages
---
## Concurrency Model Translation
### Elixir: Process-Based Concurrency
```elixir
# Multiple concurrent processes
tasks = Enum.map(user_ids, fn id ->
Task.async(fn -> fetch_user(id) end)
end)
users = Task.await_many(tasks, 5000)
```
### Elm: Command-Based Concurrency
```elm
-- Multiple HTTP requests (runtime handles concurrency)
type Msg
= FetchUsers
| GotUser Int (Result Http.Error User)
fetchUsers : List Int -> Cmd Msg
fetchUsers userIds =
userIds
|> List.map (\id ->
Http.get
{ url = "/api/users/" ++ String.fromInt id
, expect = Http.expectJson (GotUser id) userDecoder
}
)
|> Cmd.batch
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FetchUsers ->
( { model | loading = True }
, fetchUsers [ 1, 2, 3, 4, 5 ]
)
GotUser id result ->
-- Each response handled as it arrives
case result of
Ok user ->
( { model | users = Dict.insert id user model.users }
, Cmd.none
)
Err _ ->
( model, Cmd.none )
```
**Why this translation:**
- Elixir's `Task.async` → Elm's `Cmd.batch`
- Elixir manages processes explicitly → Elm runtime handles concurrency
- Both allow multiple operations in flight
- Elm guarantees serialized message handling (no race conditions)
### GenServer Periodic Work → Time Subscriptions
**Elixir:**
```elixir
defmodule PeriodicWorker do
use GenServer
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
def init(state) do
schedule_work()
{:ok, state}
end
def handle_info(:work, state) do
do_work()
schedule_work()
{:noreply, state}
end
defp schedule_work do
Process.send_after(self(), :work, 5000)
end
end
```
**Elm:**
```elm
import Time
type Msg
= DoWork Time.Posix
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every 5000 DoWork -- 5000 milliseconds
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
DoWork time ->
( model, performWork time )
performWork : Time.Posix -> Cmd Msg
performWork time =
-- Perform work here
Cmd.none
```
**Why this translation:**
- `Process.send_after` → `Time.every` subscription
- `handle_info` → `update` with time-based Msg
- No manual rescheduling needed; Sub is continuous
- Elm runtime manages subscription lifecycle
---
## Error Handling
### Elixir Error Patterns
```elixir
# Pattern 1: Tagged tuples
{:ok, value} | {:error, reason}
# Pattern 2: Raise/rescue
try do
dangerous_operation()
rescue
e in RuntimeError -> {:error, e.message}
end
# Pattern 3: Pattern match or default
user = find_user(id) || %User{name: "Anonymous"}
# Pattern 4: With statement
with {:ok, a} <- step1(),
{:ok, b} <- step2(a) do
{:ok, b}
else
{:error, reason} -> {:error, reason}
end
```
### Elm Error Patterns
```elm
-- Pattern 1: Result type
Ok value | Err reason
-- Pattern 2: No exceptions!
-- All errors are values
dangerousOperation : () -> Result String Value
dangerousOperation () =
-- Cannot throw, must return Result
-- Pattern 3: Maybe with default
user : User
user =
findUser id
|> Maybe.withDefault { name = "Anonymous" }
-- Pattern 4: Result.andThen
processData : Input -> Result String Output
processData input =
step1 input
|> Result.andThen step2
|> Result.andThen step3
```
**Key Differences:**
- Elm has NO exceptions - all errors are Result or Maybe values
- No try/rescue needed - compiler enforces error handling
- `with` statement → `Result.andThen` chaining
- Pattern matching on errors is identical
---
## Testing Strategy
### Property-Based Testing
**Elixir (StreamData):**
```elixir
defmodule MathTest do
use ExUnit.Case
use ExUnitProperties
property "addition is commutative" do
check all x <- integer(),
y <- integer() do
assert Math.add(x, y) == Math.add(y, x)
end
end
property "list reverse is idempotent" do
check all list <- list_of(integer()) do
assert list |> Enum.reverse() |> Enum.reverse() == list
end
end
end
```
**Elm (elm-test with fuzz):**
```elm
module MathTest exposing (..)
import Expect
import Fuzz exposing (int, list)
import Test exposing (Test, describe, fuzz, fuzz2)
suite : Test
suite =
describe "Math properties"
[ fuzz2 int int "addition is commutative" <|
\x y ->
Math.add x y
|> Expect.equal (Math.add y x)
, fuzz (list int) "list reverse is idempotent" <|
\list ->
list
|> List.reverse
|> List.reverse
|> Expect.equal list
]
```
**Why this translation:**
- Both support property-based testing
- `check all` → `fuzz` / `fuzz2`
- Generators translate directly
- Same test philosophy
### Unit Testing
**Elixir:**
```elixir
test "parses valid age" do
assert {:ok, 25} = parse_age("25")
end
test "rejects negative age" do
assert {:error, _} = parse_age("-5")
end
```
**Elm:**
```elm
test "parses valid age" <|
\_ ->
parseAge "25"
|> Expect.equal (Ok 25)
test "rejects negative age" <|
\_ ->
parseAge "-5"
|> Expect.err
```
---
## Common Pitfalls
### 1. Expecting Runtime Dynamism
```elixir
# Elixir: Dynamic typing allows this
defmodule Flexible do
def process(value) when is_integer(value), do: value * 2
def process(value) when is_binary(value), do: String.upcase(value)
end
process(5) # 10
process("hi") # "HI"
```
```elm
-- Elm: Must use union types for different types
type Value
= IntValue Int
| StringValue String
process : Value -> String
process value =
case value of
IntValue int ->
String.fromInt (int * 2)
StringValue str ->
String.toUpper str
-- Usage
process (IntValue 5) -- "10"
process (StringValue "hi") -- "HI"
```
**Fix:** Define explicit union types for polymorphic values.
### 2. Side Effects Anywhere vs. Managed Effects
```elixir
# Elixir: Can perform IO anywhere
def get_user(id) do
IO.puts("Fetching user #{id}") # Side effect!
Database.get(:users, id) # Side effect!
end
```
```elm
-- Elm: All effects through Cmd
getUser : Int -> Cmd Msg
getUser id =
-- Cannot perform side effects directly
-- Must return Cmd for Elm runtime
Http.get
{ url = "/api/users/" ++ String.fromInt id
, expect = Http.expectJson GotUser userDecoder
}
-- "Logging" must also be a command (via port)
port logMessage : String -> Cmd msg
getUserWithLog : Int -> Cmd Msg
getUserWithLog id =
Cmd.batch
[ logMessage ("Fetching user " ++ String.fromInt id)
, getUser id
]
```
**Fix:** Plan where effects belong in your Elm architecture.
### 3. Assuming Atoms Translate to Strings
```elixir
# Elixir: Atoms are efficient, unique
:ok
:error
:atom_name
```
```elm
-- Elm: Create custom types instead
type Status
= Ok
| Error
| AtomName
-- NOT strings!
-- type Status = String -- WRONG - loses type safety
```
**Fix:** Use union types for atom-like values, not strings.
### 4. GenServer State vs. TEA Model
```elixir
# Elixir: State hidden in process
GenServer.call(MyServer, :get_state)
```
```elm
-- Elm: State is always in Model, always visible to view
view : Model -> Html Msg
view model =
-- Direct access to all state
div [] [ text model.name ]
```
**Fix:** In Elm, embrace that all state is in Model and visible.
### 5. Expecting to "Let It Crash"
```elixir
# Elixir: Supervision restarts crashed processes
def risky_operation(value) do
# If this crashes, supervisor restarts the process
dangerous_thing(value)
end
```
```elm
-- Elm: NO CRASHES - compiler guarantees no runtime exceptions
riskyOperation : Value -> Result Error Output
riskyOperation value =
-- Must handle all error cases explicitly
case dangerousThing value of
Ok result ->
Ok result
Err error ->
Err error
```
**Fix:** Handle all error cases explicitly with Result type.
---
## Tooling
| Elixir Tool | Elm Equivalent | Notes |
|-------------|----------------|-------|
| `mix format` | `elm-format` | Auto-formatting |
| `mix test` | `elm-test` | Unit and property testing |
| `credo` | `elm-review` | Linting and code quality |
| `dialyzer` | Elm compiler | Type checking (Elm is stricter) |
| `iex` | `elm repl` | Interactive REPL |
| `mix deps.get` | `elm install` | Dependency management |
| `observer` | Browser DevTools | Runtime inspection |
| `mix docs` | `elm-doc-preview` | Documentation generation |
---
## Example: Complete Conversion
### Elixir: User Management Module
```elixir
defmodule UserManager do
use GenServer
# Client API
def start_link(_) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def add_user(user) do
GenServer.call(__MODULE__, {:add, user})
end
def get_user(id) do
GenServer.call(__MODULE__, {:get, id})
end
def list_users do
GenServer.call(__MODULE__, :list)
end
# Server Callbacks
@impl true
def init(_) do
{:ok, %{}}
end
@impl true
def handle_call({:add, user}, _from, state) do
new_state = Map.put(state, user.id, user)
{:reply, {:ok, user}, new_state}
end
def handle_call({:get, id}, _from, state) do
result = Map.get(state, id)
{:reply, result, state}
end
def handle_call(:list, _from, state) do
users = Map.values(state)
{:reply, users, state}
end
end
# Usage
{:ok, _} = UserManager.start_link([])
UserManager.add_user(%{id: 1, name: "Alice"})
UserManager.list_users()
```
### Elm: User Management in TEA
```elm
module Main exposing (main)
import Browser
import Dict exposing (Dict)
import Html exposing (Html, button, div, input, text)
import Html.Attributes exposing (placeholder, value)
import Html.Events exposing (onClick, onInput)
-- MODEL
type alias User =
{ id : Int
, name : String
}
type alias Model =
{ users : Dict Int User
, nextId : Int
, nameInput : String
}
init : () -> ( Model, Cmd Msg )
init _ =
( { users = Dict.empty
, nextId = 1
, nameInput = ""
}
, Cmd.none
)
-- UPDATE
type Msg
= AddUser
| SetNameInput String
| RemoveUser Int
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
AddUser ->
if String.isEmpty model.nameInput then
( model, Cmd.none )
else
let
user =
{ id = model.nextId
, name = model.nameInput
}
newUsers =
Dict.insert model.nextId user model.users
in
( { model
| users = newUsers
| nextId = model.nextId + 1
| nameInput = ""
}
, Cmd.none
)
SetNameInput name ->
( { model | nameInput = name }, Cmd.none )
RemoveUser id ->
( { model | users = Dict.remove id model.users }
, Cmd.none
)
-- VIEW
view : Model -> Html Msg
view model =
div []
[ div []
[ input
[ placeholder "Enter name"
, value model.nameInput
, onInput SetNameInput
]
[]
, button [ onClick AddUser ] [ text "Add User" ]
]
, div [] (viewUsers model.users)
]
viewUsers : Dict Int User -> List (Html Msg)
viewUsers users =
users
|> Dict.values
|> List.map viewUser
viewUser : User -> Html Msg
viewUser user =
div []
[ text user.name
, button [ onClick (RemoveUser user.id) ] [ text "Remove" ]
]
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
```
**Key Translation Points:**
1. GenServer state → Model record
2. `handle_call` → `update` function branches
3. Client API → Msg variants
4. Synchronous calls → Direct model access in view
5. Map-based storage → Dict in Elm
---
## See Also
For more examples and patterns, see:
- `meta-convert-dev` - Foundational patterns with cross-language examples
- `convert-elm-scala` - Related conversion from Elm
- `convert-haskell-elm` - Similar pure functional → frontend conversion
- `lang-elixir-dev` - Elixir development patterns
- `lang-elm-dev` - Elm development patterns
Cross-cutting pattern skills:
- `patterns-concurrency-dev` - Processes, async, and message passing across languages
- `patterns-serialization-dev` - JSON handling and data encoding patterns
- `patterns-metaprogramming-dev` - Compare dynamic features to type-driven design
No comments yet. Be the first to comment!