| 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
- Identify pure business logic - Elm runs in browser, not on BEAM VM
- Map dynamic types to static - Add explicit type annotations
- Convert processes to TEA - GenServers become Model-View-Update
- Translate effects - Side effects become Cmd, subscriptions become Sub
- Preserve semantics - Both are functional, immutable languages
- Handle compilation errors first - Elm compiler guides you to correctness
- 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:
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:
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'sResult error value - Guards in Elixir (
when b != 0) become if-expressions in Elm - Atoms like
:division_by_zerobecome strings or custom types - Both use pattern matching in case expressions
Pattern 2: Nil Handling → Maybe
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:
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
nilbecomes Elm'sNothing - Present values become
Just value Maybe.withDefaultreplaces nil coalescing- Pattern matching translates directly
Pattern 3: Enum Operations → List Functions
Elixir:
[1, 2, 3, 4, 5]
|> Enum.filter(&(rem(&1, 2) == 0))
|> Enum.map(&(&1 * 2))
|> Enum.reduce(0, &(&1 + &2))
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.filterEnum.map→List.mapEnum.reduce→List.foldl(orList.foldr)- Pipeline operator
|>is identical - Capture operator
&()becomes lambda\x -> rembecomesmodByin Elm
Pattern 4: Pattern Matching Lists
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:
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 :: tailin Elm- Multiple function clauses become case branches
- Guards can become if-expressions inside branches
Pattern 5: With Statement → Nested Case or Result.andThen
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:
-- 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
withchains{:ok, _}results - Elm uses
Result.andThenfor 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):
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):
-- 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→updatefunction branches- Synchronous replies → View reads model directly
- No process needed; Elm runtime manages state
Process Communication → Cmd/Sub
Elixir (Process Messages):
# 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):
-- 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 updatereceive→ 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):
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):
-- 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
# 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
-- 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'sCmd.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:
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:
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.everysubscriptionhandle_info→updatewith time-based Msg- No manual rescheduling needed; Sub is continuous
- Elm runtime manages subscription lifecycle
Error Handling
Elixir Error Patterns
# 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
-- 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
withstatement →Result.andThenchaining- Pattern matching on errors is identical
Testing Strategy
Property-Based Testing
Elixir (StreamData):
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):
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:
test "parses valid age" do
assert {:ok, 25} = parse_age("25")
end
test "rejects negative age" do
assert {:error, _} = parse_age("-5")
end
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: 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: 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: Can perform IO anywhere
def get_user(id) do
IO.puts("Fetching user #{id}") # Side effect!
Database.get(:users, id) # Side effect!
end
-- 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: Atoms are efficient, unique
:ok
:error
:atom_name
-- 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: State hidden in process
GenServer.call(MyServer, :get_state)
-- 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: Supervision restarts crashed processes
def risky_operation(value) do
# If this crashes, supervisor restarts the process
dangerous_thing(value)
end
-- 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
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
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:
- GenServer state → Model record
handle_call→updatefunction branches- Client API → Msg variants
- Synchronous calls → Direct model access in view
- Map-based storage → Dict in Elm
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language examplesconvert-elm-scala- Related conversion from Elmconvert-haskell-elm- Similar pure functional → frontend conversionlang-elixir-dev- Elixir development patternslang-elm-dev- Elm development patterns
Cross-cutting pattern skills:
patterns-concurrency-dev- Processes, async, and message passing across languagespatterns-serialization-dev- JSON handling and data encoding patternspatterns-metaprogramming-dev- Compare dynamic features to type-driven design