xiaohongshu
xiaohongshu
convert-elm-elixir
Convert Elm code to idiomatic Elixir. Use when migrating Elm frontend applications to Elixir (Phoenix LiveView), translating Elm's functional patterns to Elixir, or refactoring Elm codebases to leverage OTP. Extends meta-convert-dev with Elm-to-Elixir specific patterns.
Full skill instructions
Convert Elm code to idiomatic Elixir. This skill extends meta-convert-dev with Elm-to-Elixir specific type mappings, idiom translations, and tooling for migrating from frontend-focused functional programming to backend/full-stack development with OTP.
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.
meta-convert-devlang-elm-devlang-elixir-devconvert-elixir-elmlang-elixir-phoenix-dev| Elm | Elixir | Notes |
|---|---|---|
String | String.t() / binary() | UTF-8 binaries |
Int | integer() | Arbitrary precision |
Float | float() | 64-bit double |
Bool | boolean() | true / false |
List a | list(a) | Linked lists |
Maybe a | nil / {:ok, a} / {:error, reason} | Context-dependent |
Result err ok | {:ok, ok} / {:error, err} | Tagged tuples |
( a, b ) | {a, b} | Tuples |
type alias | defstruct / @type | Records → Structs |
type (union) | @type with | or defmodule enum pattern | Sum types |
Cmd msg | GenServer.cast/2 or async process | Side effects |
Sub msg | GenServer callbacks / PubSub | Event subscriptions |
Model | GenServer state / LiveView assigns | Application state |
view | LiveView render/1 | UI rendering |
| Elm | Elixir | Notes |
|---|---|---|
String | String.t() | Both UTF-8, Elixir uses binaries |
Int | integer() | Elm uses 32-bit JS numbers, Elixir has arbitrary precision |
Float | float() | IEEE 754 double precision in both |
Bool | boolean() | Direct mapping |
Char | String.t() (single char) | Elixir doesn't have char type; use single-char string |
() (unit) | :ok / nil | Context-dependent; often :ok for success |
| Elm | Elixir | Notes |
|---|---|---|
List a | list(a) | Both are linked lists |
Array a | list(a) | Elm's Array is optimized, Elixir uses lists or :array module |
( a, b ) | {a, b} | Tuples map directly |
( a, b, c ) | {a, b, c} | Tuples support any arity |
Dict k v | Map.t(k, v) | Elm Dict → Elixir Map |
Set a | MapSet.t(a) | Set implementations |
| Elm | Elixir | Notes |
|---|---|---|
type alias User = { name : String, ... } | defstruct [:name, ...] + @type | Record → Struct |
type Msg = Inc | Dec | @type msg :: :inc | :dec or atoms | Union types → Atoms or tagged tuples |
type Msg = SetName String | {:set_name, String.t()} | Tagged tuple pattern |
Maybe a | nil | a or {:ok, a} | {:error, term()} | Elm Maybe → Elixir optionals |
Result err ok | {:ok, ok} | {:error, err} | Elm Result → Elixir result tuples |
| Elm TEA | Elixir (LiveView) | Notes |
|---|---|---|
Model | socket.assigns | State in LiveView socket |
Msg | Event names (atoms) | Messages sent to LiveView |
init : flags -> ( Model, Cmd Msg ) | mount/3 | Initialize state |
update : Msg -> Model -> ( Model, Cmd Msg ) | handle_event/3 | Handle user events |
view : Model -> Html Msg | render/1 | Render UI |
Cmd Msg | send(self(), msg) or async tasks | Side effects |
Sub Msg | PubSub.subscribe/2 | Event subscriptions |
| Elm TEA | Elixir (GenServer) | Notes |
|---|---|---|
Model | GenServer state | Application state |
Msg | Messages to GenServer | Pattern-matched in callbacks |
init | init/1 callback | Initialize GenServer |
update | handle_call/3 or handle_cast/2 | Handle messages |
Cmd | Async process / Task | Side effects |
Sub | handle_info/2 | Receive external messages |
Elm:
type Maybe a
= Just a
| Nothing
findUser : Int -> Maybe User
findUser id =
users
|> List.filter (\u -> u.id == id)
|> List.head
case findUser 1 of
Just user ->
user.name
Nothing ->
"Anonymous"
Elixir:
# Using nil
def find_user(id) do
Enum.find(users(), fn u -> u.id == id end)
end
case find_user(1) do
nil -> "Anonymous"
user -> user.name
end
# Or using result tuples (more idiomatic for operations that can fail)
def find_user(id) do
case Enum.find(users(), fn u -> u.id == id end) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
case find_user(1) do
{:ok, user} -> user.name
{:error, :not_found} -> "Anonymous"
end
Why this translation:
Maybe is explicit about presence/absencenil for simple optionals or {:ok, value} / {:error, reason} for operationsElm:
type Result error value
= Ok value
| Err error
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"
case parseAge "25" of
Ok age ->
"Age: " ++ String.fromInt age
Err message ->
"Error: " ++ message
Elixir:
@spec parse_age(String.t()) :: {:ok, integer()} | {:error, String.t()}
def parse_age(str) do
case Integer.parse(str) do
{age, ""} when age >= 0 ->
{:ok, age}
{_age, ""} ->
{:error, "Age must be non-negative"}
_ ->
{:error, "Not a valid number"}
end
end
case parse_age("25") do
{:ok, age} ->
"Age: #{age}"
{:error, message} ->
"Error: #{message}"
end
Why this translation:
Result to Elixir tagged tupleswith statement can chain multiple results (similar to Elm's Result.andThen)Elm (Counter):
module Main exposing (main)
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
-- MODEL
type alias Model =
{ count : Int }
init : () -> ( Model, Cmd Msg )
init _ =
( { count = 0 }, Cmd.none )
-- UPDATE
type Msg
= Increment
| Decrement
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 )
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model.count) ]
, button [ onClick Increment ] [ text "+" ]
]
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
Elixir (Phoenix LiveView):
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
# MOUNT (like init)
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
# HANDLE_EVENT (like update)
@impl true
def handle_event("increment", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
def handle_event("decrement", _params, socket) do
{:noreply, update(socket, :count, &(&1 - 1))}
end
# RENDER (like view)
@impl true
def render(assigns) do
~H"""
<div>
<button phx-click="decrement">-</button>
<div><%= @count %></div>
<button phx-click="increment">+</button>
</div>
"""
end
end
Why this translation:
socket.assignsupdate → LiveView's handle_event/3view → LiveView's render/1Cmd → LiveView async operations (via send(self(), ...) or Task.async)Elm:
processData : List Int -> Int
processData numbers =
numbers
|> List.filter (\x -> x > 0)
|> List.map (\x -> x * 2)
|> List.foldl (+) 0
Elixir:
def process_data(numbers) do
numbers
|> Enum.filter(&(&1 > 0))
|> Enum.map(&(&1 * 2))
|> Enum.sum()
end
# Or using reduce explicitly
def process_data(numbers) do
numbers
|> Enum.filter(&(&1 > 0))
|> Enum.map(&(&1 * 2))
|> Enum.reduce(0, &+/2)
end
Why this translation:
List module → Enum module\x -> x * 2 → Elixir &(&1 * 2) or fn x -> x * 2 endElm:
type Status
= Loading
| Success String
| Failure String
handleStatus : Status -> String
handleStatus status =
case status of
Loading ->
"Loading..."
Success data ->
"Data: " ++ data
Failure error ->
"Error: " ++ error
Elixir:
# Using atoms and tagged tuples
@type status :: :loading | {:success, String.t()} | {:failure, String.t()}
def handle_status(status) do
case status do
:loading ->
"Loading..."
{:success, data} ->
"Data: #{data}"
{:failure, error} ->
"Error: #{error}"
end
end
Why this translation:
| Aspect | Elm | Elixir |
|---|---|---|
| Primary Model | Maybe / Result | Tagged tuples ({:ok, val} / {:error, reason}) |
| Error Propagation | Result.andThen | with statement |
| Null Safety | No nulls; use Maybe | nil exists but tuples preferred |
| Exceptions | None (compile-time guarantee) | Exceptions exist but discouraged; use {:error, reason} |
Elm:
validateAndSave : String -> Result String User
validateAndSave input =
input
|> validateEmail
|> Result.andThen createUser
|> Result.andThen saveToDatabase
case validateAndSave "[email protected]" of
Ok user ->
"Saved: " ++ user.name
Err message ->
"Failed: " ++ message
Elixir:
def validate_and_save(input) do
with {:ok, email} <- validate_email(input),
{:ok, user} <- create_user(email),
{:ok, saved_user} <- save_to_database(user) do
{:ok, saved_user}
else
{:error, reason} -> {:error, reason}
end
end
case validate_and_save("[email protected]") do
{:ok, user} ->
"Saved: #{user.name}"
{:error, message} ->
"Failed: #{message}"
end
Why this translation:
Result.andThen for chaining → Elixir's with statementwith is more flexible (can handle multiple patterns)Elm's concurrency is managed by the Elm Runtime - you describe side effects declaratively via Cmd and Sub. Elixir requires explicit process management but provides OTP primitives.
Elm:
-- Cmd describes side effect; runtime executes it
type Msg
= GotUsers (Result Http.Error (List User))
getUsers : Cmd Msg
getUsers =
Http.get
{ url = "https://api.example.com/users"
, expect = Http.expectJson GotUsers (Decode.list userDecoder)
}
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 _ ->
( { model | error = Just "Failed", loading = False }, Cmd.none )
Elixir (LiveView):
defmodule MyAppWeb.UsersLive do
use MyAppWeb, :live_view
def handle_event("fetch_users", _params, socket) do
# Async task (like Cmd)
send(self(), :perform_fetch)
{:noreply, assign(socket, loading: true)}
end
def handle_info(:perform_fetch, socket) do
case HTTPoison.get("https://api.example.com/users") do
{:ok, %{body: body}} ->
users = Jason.decode!(body)
{:noreply, assign(socket, users: users, loading: false)}
{:error, _reason} ->
{:noreply, assign(socket, error: "Failed", loading: false)}
end
end
end
Elixir (GenServer):
defmodule UserFetcher do
use GenServer
def handle_cast(:fetch_users, state) do
# Spawn async task
Task.async(fn ->
HTTPoison.get("https://api.example.com/users")
end)
{:noreply, %{state | loading: true}}
end
def handle_info({_ref, {:ok, %{body: body}}}, state) do
users = Jason.decode!(body)
{:noreply, %{state | users: users, loading: false}}
end
def handle_info({_ref, {:error, _reason}}, state) do
{:noreply, %{state | error: "Failed", loading: false}}
end
end
Why this translation:
Cmd → Elixir send(self(), msg) or Task.asyncupdate calls → Elixir GenServer/LiveView handle one message at a timeElm:
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every 1000 Tick -- Every second
Elixir (LiveView):
def mount(_params, _session, socket) do
if connected?(socket) do
# Subscribe to Phoenix PubSub
Phoenix.PubSub.subscribe(MyApp.PubSub, "events")
# Or schedule periodic message
:timer.send_interval(1000, self(), :tick)
end
{:ok, socket}
end
def handle_info(:tick, socket) do
# Handle periodic event
{:noreply, update(socket, :count, &(&1 + 1))}
end
def handle_info(%{event: "user_updated", payload: user}, socket) do
# Handle PubSub message
{:noreply, assign(socket, current_user: user)}
end
Why this translation:
Sub → Elixir Phoenix.PubSub or :timer or handle_info/2Assuming Elm's "No Runtime Errors" in Elixir
@spec), Dialyzer, and pattern match exhaustivelyForgetting Immutability Differences
Misunderstanding Cmd/Sub → Process Translation
Over-using Exceptions
{:ok, val} / {:error, reason}Ignoring OTP Supervision
Not Leveraging Elixir's Strengths
Type Alias vs Struct Confusion
type alias creates a record typedefstruct creates a module-specific structdefstruct for domain models, @type for type annotations| Tool | Purpose | Notes |
|---|---|---|
| Phoenix | Web framework | Use for full-stack apps; LiveView for reactive UIs |
| Phoenix LiveView | Reactive UI | Closest equivalent to Elm's TEA |
| Ecto | Database ORM | No direct Elm equivalent (frontend-only) |
| ExUnit | Testing framework | Similar philosophy to elm-test |
| Dialyzer | Static analysis | Partial type checking (not as strong as Elm's) |
| Credo | Code linter | Enforce Elixir conventions |
| mix format | Code formatter | Like elm-format |
Elm:
type alias User =
{ name : String
, email : String
, age : Int
}
createUser : String -> String -> Int -> User
createUser name email age =
{ name = name, email = email, age = age }
Elixir:
defmodule User do
@type t :: %__MODULE__{
name: String.t(),
email: String.t(),
age: integer()
}
defstruct [:name, :email, :age]
@spec create(String.t(), String.t(), integer()) :: t()
def create(name, email, age) do
%User{name: name, email: email, age: age}
end
end
Elm:
type alias ValidationError = String
validateUser : String -> String -> Int -> Result ValidationError User
validateUser name email age =
validateName name
|> Result.andThen (\_ -> validateEmail email)
|> Result.andThen (\_ -> validateAge age)
|> Result.map (\_ -> { name = name, email = email, age = age })
validateName : String -> Result ValidationError String
validateName name =
if String.isEmpty name then
Err "Name cannot be empty"
else
Ok name
validateEmail : String -> Result ValidationError String
validateEmail email =
if String.contains email "@" then
Ok email
else
Err "Invalid email"
validateAge : Int -> Result ValidationError Int
validateAge age =
if age >= 18 then
Ok age
else
Err "Must be at least 18"
Elixir:
defmodule UserValidator do
@type validation_error :: String.t()
@spec validate_user(String.t(), String.t(), integer()) ::
{:ok, User.t()} | {:error, validation_error()}
def validate_user(name, email, age) do
with {:ok, _} <- validate_name(name),
{:ok, _} <- validate_email(email),
{:ok, _} <- validate_age(age) do
{:ok, User.create(name, email, age)}
end
end
defp validate_name(""), do: {:error, "Name cannot be empty"}
defp validate_name(name), do: {:ok, name}
defp validate_email(email) do
if String.contains?(email, "@") do
{:ok, email}
else
{:error, "Invalid email"}
end
end
defp validate_age(age) when age >= 18, do: {:ok, age}
defp validate_age(_), do: {:error, "Must be at least 18"}
end
Elm (Todo App):
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
-- MODEL
type alias Model =
{ todos : List Todo
, input : String
}
type alias Todo =
{ id : Int
, text : String
, completed : Bool
}
init : () -> ( Model, Cmd Msg )
init _ =
( { todos = [], input = "" }, Cmd.none )
-- UPDATE
type Msg
= UpdateInput String
| AddTodo
| ToggleTodo Int
| RemoveTodo Int
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateInput input ->
( { model | input = input }, Cmd.none )
AddTodo ->
if String.isEmpty model.input then
( model, Cmd.none )
else
let
newTodo =
{ id = List.length model.todos
, text = model.input
, completed = False
}
in
( { model
| todos = model.todos ++ [ newTodo ]
, input = ""
}
, Cmd.none
)
ToggleTodo id ->
let
toggleTodo todo =
if todo.id == id then
{ todo | completed = not todo.completed }
else
todo
in
( { model | todos = List.map toggleTodo model.todos }, Cmd.none )
RemoveTodo id ->
( { model | todos = List.filter (\t -> t.id /= id) model.todos }
, Cmd.none
)
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input
[ type_ "text"
, placeholder "Add todo"
, value model.input
, onInput UpdateInput
]
[]
, button [ onClick AddTodo ] [ text "Add" ]
, ul [] (List.map viewTodo model.todos)
]
viewTodo : Todo -> Html Msg
viewTodo todo =
li []
[ input
[ type_ "checkbox"
, checked todo.completed
, onClick (ToggleTodo todo.id)
]
[]
, span
[ style "text-decoration"
(if todo.completed then "line-through" else "none")
]
[ text todo.text ]
, button [ onClick (RemoveTodo todo.id) ] [ text "X" ]
]
Elixir (Phoenix LiveView Todo App):
defmodule MyAppWeb.TodoLive do
use MyAppWeb, :live_view
# Domain model
defmodule Todo do
@type t :: %__MODULE__{
id: integer(),
text: String.t(),
completed: boolean()
}
defstruct [:id, :text, :completed]
end
# MOUNT (init)
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, todos: [], input: "")}
end
# HANDLE_EVENT (update)
@impl true
def handle_event("update_input", %{"value" => input}, socket) do
{:noreply, assign(socket, input: input)}
end
def handle_event("add_todo", _params, socket) do
input = socket.assigns.input
if String.trim(input) == "" do
{:noreply, socket}
else
todos = socket.assigns.todos
new_todo = %Todo{
id: length(todos),
text: input,
completed: false
}
{:noreply, assign(socket, todos: todos ++ [new_todo], input: "")}
end
end
def handle_event("toggle_todo", %{"id" => id}, socket) do
id = String.to_integer(id)
todos = Enum.map(socket.assigns.todos, fn todo ->
if todo.id == id do
%{todo | completed: !todo.completed}
else
todo
end
end)
{:noreply, assign(socket, todos: todos)}
end
def handle_event("remove_todo", %{"id" => id}, socket) do
id = String.to_integer(id)
todos = Enum.reject(socket.assigns.todos, &(&1.id == id))
{:noreply, assign(socket, todos: todos)}
end
# RENDER (view)
@impl true
def render(assigns) do
~H"""
<div>
<input
type="text"
placeholder="Add todo"
value={@input}
phx-keyup="update_input"
/>
<button phx-click="add_todo">Add</button>
<ul>
<%= for todo <- @todos do %>
<li>
<input
type="checkbox"
checked={todo.completed}
phx-click="toggle_todo"
phx-value-id={todo.id}
/>
<span style={"text-decoration: #{if todo.completed, do: "line-through", else: "none"}"}>
<%= todo.text %>
</span>
<button phx-click="remove_todo" phx-value-id={todo.id}>X</button>
</li>
<% end %>
</ul>
</div>
"""
end
end
Router configuration:
# In router.ex
scope "/", MyAppWeb do
pipe_through :browser
live "/todos", TodoLive
end
For more examples and patterns, see:
meta-convert-dev - Foundational patterns with cross-language exampleslang-elm-dev - Elm development patterns and TEAlang-elixir-dev - Elixir development patternslang-elixir-phoenix-dev - Advanced Phoenix and LiveView patternslang-elixir-otp-dev - OTP patterns for concurrencyCross-cutting pattern skills:
patterns-concurrency-dev - Compare Elm Cmd/Sub to Elixir processes/GenServerpatterns-serialization-dev - JSON encoding/decoding across languagespatterns-testing-dev - Testing strategies for functional codexiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience