xiaohongshu
xiaohongshu
convert-roc-elm
Convert Roc code to idiomatic Elm. Use when migrating Roc applications to Elm frontend code, translating platform-agnostic Roc to browser-based Elm, or refactoring Roc CLI tools to Elm web applications. Extends meta-convert-dev with Roc-to-Elm specific patterns.
Full skill instructions
Convert Roc code to idiomatic Elm. This skill extends meta-convert-dev with Roc-to-Elm specific type mappings, idiom translations, and architectural patterns for moving from platform-agnostic Roc to browser-based Elm applications.
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-roc-devlang-elm-devconvert-elm-roc| Roc | Elm | Notes |
|---|---|---|
Str | String | Direct mapping |
I64, U64 | Int | Elm has arbitrary precision integers |
F64 | Float | Direct mapping |
Bool | Bool | Direct mapping with capitalization |
List a | List a | Same syntax and operations |
{ field : Type } | { field : Type } | Records are nearly identical |
[Tag1, Tag2] | type Custom = Tag1 | Tag2 | Tag unions → Custom types |
Result a e | Result e a | Reversed parameter order! |
[Some a, None] | Maybe a | Optional values |
Task a err | Cmd Msg or Task Never a | Effect systems differ |
when x is | case x of | Pattern matching syntax |
! suffix operator | Task.perform | Bang operator → explicit Task handling |
| Aspect | Roc Platform Model | Elm TEA |
|---|---|---|
| Target | Any platform (CLI, web, native) | Browser frontend only |
| Effects | Platform-provided Task | Runtime-managed Cmd/Sub |
| Entry point | main : Task {} [] | main : Program () Model Msg |
| State | Implicit in Task chain | Explicit Model |
| Updates | Task composition | update : Msg → Model → (Model, Cmd Msg) |
| I/O | Platform exposes (File, Http, etc.) | Browser.* modules only |
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/..."
}
import pf.Stdout
import pf.Task exposing [Task]
main : Task {} []
main =
Stdout.line! "Hello, World!"
name = Stdin.line!
Stdout.line! "Hello, \(name)!"
module Main exposing (main)
import Browser
import Html exposing (Html, div, input, text)
import Html.Events exposing (onInput)
import Html.Attributes exposing (placeholder, value)
-- MODEL
type alias Model =
{ name : String
, greeting : String
}
init : () -> ( Model, Cmd Msg )
init _ =
( { name = "", greeting = "Hello, World!" }, Cmd.none )
-- UPDATE
type Msg
= NameChanged String
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NameChanged newName ->
( { model
| name = newName
, greeting = "Hello, " ++ newName ++ "!"
}
, Cmd.none
)
-- VIEW
view : Model -> Html Msg
view model =
div []
[ div [] [ text model.greeting ]
, input
[ placeholder "Enter your name"
, value model.name
, onInput NameChanged
]
[]
]
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
Key shift: Roc's imperative Task chain becomes Elm's declarative Model-Update-View cycle.
| Roc | Elm | Notes |
|---|---|---|
Bool.true / Bool.false | True / False | Capitalization differs |
42 | 42 | Integer literals (Elm has arbitrary precision) |
3.14 | 3.14 | Float literals |
"text" | "text" | String literals (Roc uses Str, Elm uses String) |
I8, I16, I32, I64, I128 | Int | Elm has single Int type (arbitrary precision) |
U8, U16, U32, U64, U128 | Int | Same - map to Int |
F32, F64 | Float | Elm has single Float type |
Num a | number | Flexible number type (inferred) |
| Roc | Elm | Notes |
|---|---|---|
List a | List a | Identical syntax and semantics |
Dict k v | Dict k v | Same interface, import from Dict module |
Set a | Set a | Same interface, import from Set module |
(a, b) | ( a, b ) | Tuples (Elm supports up to 3-tuples idiomatically) |
| Roc | Elm | Notes |
|---|---|---|
{ name : Str, age : U32 } | { name : String, age : Int } | Nearly identical, just type name differences |
{ user & age: 31 } | { user | age = 31 } | Record update syntax differs (& vs |) |
{ name, age } = user | { name, age } = user | Destructuring identical |
user.name | user.name | Field access identical |
Roc:
# Structural tag union
Color : [Red, Green, Blue, Custom(U8, U8, U8)]
handleColor : Color -> Str
handleColor = \color ->
when color is
Red -> "red"
Green -> "green"
Blue -> "blue"
Custom(r, g, b) -> "rgb(\(Num.toStr(r)), ...)"
Elm:
-- Named custom type (nominal)
type Color
= Red
| Green
| Blue
| Custom Int Int Int
handleColor : Color -> String
handleColor color =
case color of
Red ->
"red"
Green ->
"green"
Blue ->
"blue"
Custom r g b ->
"rgb(" ++ String.fromInt r ++ ", ...)"
Key differences:
type declarationRoc:
# Inline tag union
email : [Some Str, None]
email = Some("[email protected]")
# Pattern match
emailText = when email is
Some(addr) -> addr
None -> "no email"
Elm:
-- Built-in Maybe type
email : Maybe String
email = Just "[email protected]"
-- Pattern match
emailText : String
emailText =
case email of
Just addr ->
addr
Nothing ->
"no email"
-- Helper functions
emailOrDefault : String
emailOrDefault =
Maybe.withDefault "no email" email
Translation:
[Some a, None] → Maybe aSome(value) → Just valueNone → NothingRoc:
# Result ok err
divide : I64, I64 -> Result I64 [DivByZero]
divide = \a, b ->
if b == 0 then
Err(DivByZero)
else
Ok(a // b)
Elm:
-- Result error ok (REVERSED!)
divide : Int -> Int -> Result String Int
divide a b =
if b == 0 then
Err "Division by zero"
else
Ok (a // b)
CRITICAL: Roc's Result ok err becomes Elm's Result error ok - parameters are reversed!
Roc:
classify : I64 -> Str
classify = \n ->
when n is
0 -> "zero"
x if x < 0 -> "negative"
_ -> "positive"
Elm:
classify : Int -> String
classify n =
case n of
0 ->
"zero"
x ->
if x < 0 then
"negative"
else
"positive"
Why this translation:
when becomes Elm's caseif after pattern), Elm uses if expressions in branches-> and indentationRoc:
doubled : List I64
doubled = List.map([1, 2, 3, 4, 5], \x -> x * 2)
# Pipeline style
result = [1, 2, 3, 4, 5]
|> List.map(\x -> x * 2)
|> List.keepIf(\x -> x > 5)
|> List.walk(0, Num.add)
Elm:
doubled : List Int
doubled =
List.map (\x -> x * 2) [ 1, 2, 3, 4, 5 ]
-- Pipeline style (same!)
result : Int
result =
[ 1, 2, 3, 4, 5 ]
|> List.map (\x -> x * 2)
|> List.filter (\x -> x > 5)
|> List.foldl (+) 0
Why this translation:
List.keepIf → List.filter (different name)List.walk → List.foldl or List.foldr (different name)|> is identicalRoc:
user = { name: "Alice", age: 30 }
olderUser = { user & age: 31 }
# Multiple fields
updatedUser = { user &
age: 31,
name: "Alice Smith"
}
Elm:
user =
{ name = "Alice", age = 30 }
olderUser =
{ user | age = 31 }
-- Multiple fields
updatedUser =
{ user
| age = 31
, name = "Alice Smith"
}
Why this translation:
& for updates, Elm uses |: for field assignment, Elm uses =Roc:
main : Task {} []
main =
content = File.readUtf8!("input.txt")
processed = String.toUpper(content)
File.writeUtf8!("output.txt", processed)
Stdout.line!("Done!")
Elm:
-- Elm doesn't have file access (browser only)
-- This example shows HTTP instead
type Msg
= GotData (Result Http.Error String)
| DataProcessed
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FetchData ->
( { model | loading = True }
, Http.get
{ url = "/api/data"
, expect = Http.expectString GotData
}
)
GotData result ->
case result of
Ok content ->
( { model
| data = String.toUpper content
, loading = False
}
, Cmd.none
)
Err _ ->
( { model | error = Just "Failed to load" }
, Cmd.none
)
Why this translation:
! suffix operator becomes explicit Cmd in ElmRoc:
calculate : I64, I64, I64 -> Result I64 [DivByZero]
calculate = \a, b, c ->
x = divide!(a, b) # Returns early on Err
y = divide!(x, c) # Returns early on Err
Ok(y)
Elm:
calculate : Int -> Int -> Int -> Result String Int
calculate a b c =
divide a b
|> Result.andThen (\x -> divide x c)
-- Or with explicit pattern matching
calculateExplicit : Int -> Int -> Int -> Result String Int
calculateExplicit a b c =
case divide a b of
Err e ->
Err e
Ok x ->
case divide x c of
Err e ->
Err e
Ok y ->
Ok y
Why this translation:
! operator doesn't exist in ElmResult.andThen for chaining (monadic bind)case expressionsRoc:
Age := U32
createAge : U32 -> Age
createAge = \n -> @Age(n)
getAge : Age -> U32
getAge = \@Age(n) -> n
Elm:
-- Elm uses module visibility for opacity
module Age exposing (Age, create, toInt)
type Age
= Age Int
create : Int -> Maybe Age
create n =
if n >= 0 && n < 150 then
Just (Age n)
else
Nothing
toInt : Age -> Int
toInt (Age n) =
n
-- Constructor Age is NOT exposed, only create function
Why this translation:
@ unwrapping syntax, Elm uses pattern matchingAge type exposed, constructor hidden)| Roc Concept | Elm Approach | Key Insight |
|---|---|---|
| Task chain (sequential) | Model-Update-View loop | Imperative → Declarative |
| Platform provides I/O | Browser provides events | CLI/Native → Browser |
main returns Task | main returns Program | Effect → Pure |
Tasks compose with ! | Cmd issued, Msg received | Direct → Indirect |
| Roc Model | Elm Model | Conceptual Translation |
|---|---|---|
| Platform handles Tasks | Runtime handles Cmd/Sub | Both managed by runtime |
Sequential with ! | Asynchronous with Msg | Chain → Event-driven |
| Task.ok/Task.err | Cmd.none / new Cmd | Return value → Side effect |
Key Difference: Parameter order is reversed!
-- Roc: Result ok err
parseAge : Str -> Result U32 [ParseError Str, NegativeAge]
parseAge = \str ->
when Str.toU32(str) is
Ok(age) if age >= 0 -> Ok(age)
Ok(_) -> Err(NegativeAge)
Err(_) -> Err(ParseError("Not a number"))
-- Elm: Result err ok (REVERSED!)
type ParseError
= NotANumber
| NegativeAge
parseAge : String -> Result ParseError Int
parseAge str =
case String.toInt str of
Just age ->
if age >= 0 then
Ok age
else
Err NegativeAge
Nothing ->
Err NotANumber
Roc uses inline tag unions:
fetchUser : U64 -> Task User [NetworkError, NotFound, Unauthorized]
Elm uses named custom types:
type FetchError
= NetworkError Http.Error
| NotFound
| Unauthorized
fetchUser : Int -> Task FetchError User
Roc Task:
# Platform-provided, sequential execution
fetchAndProcess : Task Str []
fetchAndProcess =
data = Http.get!("https://api.example.com/data")
processed = String.toUpper(data)
Task.ok(processed)
Elm Cmd (most common):
type Msg
= GotData (Result Http.Error String)
fetchData : Cmd Msg
fetchData =
Http.get
{ url = "https://api.example.com/data"
, expect = Http.expectString GotData
}
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FetchData ->
( model, fetchData )
GotData result ->
case result of
Ok data ->
( { model | data = String.toUpper data }
, Cmd.none
)
Err _ ->
( { model | error = Just "Failed" }
, Cmd.none
)
Elm Task (advanced):
import Task
-- For chaining async operations
fetchAndProcess : Task Http.Error String
fetchAndProcess =
Http.task
{ method = "GET"
, headers = []
, url = "https://api.example.com/data"
, body = Http.emptyBody
, resolver = Http.stringResolver handleResponse
, timeout = Nothing
}
|> Task.map String.toUpper
-- Convert to Cmd
performFetch : Cmd Msg
performFetch =
Task.attempt GotData fetchAndProcess
Result parameter order reversal
Result ok errResult err okRecord update syntax
{ record & field: value }{ record | field = value }&/| and :/=Case vs When syntax
when x iscase x ofof not isModule target mismatch
Bang operator translation
value = task!Task.andThen or Cmd with Msg handling! operatorCapitalization
Bool.trueTrueFunction application
List.map(list, fn) or List.map list fnList.map fn list (function first)| Tool | Roc | Elm | Notes |
|---|---|---|---|
| Formatter | roc format | elm-format | Both enforce standard style |
| REPL | roc repl | elm repl | Both support interactive testing |
| Test | roc test | elm-test | Different syntax, same concept |
| Build | roc build | elm make | Roc → native, Elm → JavaScript |
| Package manager | Platform URLs | elm install | Elm has centralized package repo |
| Linter | N/A | elm-review | Elm has rich linting ecosystem |
Before (Roc):
User : { name : Str, age : U32 }
greet : User -> Str
greet = \user ->
"Hello, \(user.name)! You are \(Num.toStr(user.age)) years old."
expect greet({ name: "Alice", age: 30 }) == "Hello, Alice! You are 30 years old."
After (Elm):
type alias User =
{ name : String
, age : Int
}
greet : User -> String
greet user =
"Hello, " ++ user.name ++ "! You are " ++ String.fromInt user.age ++ " years old."
-- Test (in tests/ directory)
import Test exposing (test)
import Expect
suite =
test "greet formats message correctly" <|
\_ ->
greet { name = "Alice", age = 30 }
|> Expect.equal "Hello, Alice! You are 30 years old."
Key changes:
Str → String, U32 → Int\(...) → concatenation ++Num.toStr → String.fromIntexpect → separate test file with Test moduleBefore (Roc):
Color : [Red, Green, Blue, Custom(U8, U8, U8)]
toHex : Color -> Str
toHex = \color ->
when color is
Red -> "#FF0000"
Green -> "#00FF00"
Blue -> "#0000FF"
Custom(r, g, b) ->
"#\(toHexByte(r))\(toHexByte(g))\(toHexByte(b))"
toHexByte : U8 -> Str
toHexByte = \n ->
# Implementation details...
After (Elm):
type Color
= Red
| Green
| Blue
| Custom Int Int Int
toHex : Color -> String
toHex color =
case color of
Red ->
"#FF0000"
Green ->
"#00FF00"
Blue ->
"#0000FF"
Custom r g b ->
"#" ++ toHexByte r ++ toHexByte g ++ toHexByte b
toHexByte : Int -> String
toHexByte n =
-- Implementation using Hex library or String.fromInt with base conversion
String.fromInt n -- Simplified for example
Key changes:
type declarationwhen x is → case x ofU8 → Int (Elm doesn't have sized integers)Before (Roc):
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/..."
}
import pf.Http
import pf.Stdout
import pf.Task exposing [Task]
User : { id : U64, name : Str, email : Str }
fetchUser : U64 -> Task User [HttpError]
fetchUser = \userId ->
url = "https://api.example.com/users/\(Num.toStr(userId))"
response = Http.get!(url)
when Decode.fromBytes(response.body, userDecoder) is
Ok(user) -> Task.ok(user)
Err(_) -> Task.err(HttpError)
main : Task {} []
main =
user = fetchUser!(1)
Stdout.line!("User: \(user.name) (\(user.email))")
After (Elm):
module Main exposing (main)
import Browser
import Html exposing (Html, div, text, button)
import Html.Events exposing (onClick)
import Http
import Json.Decode as Decode exposing (Decoder)
-- MODEL
type alias User =
{ id : Int
, name : String
, email : String
}
type RemoteData e a
= NotAsked
| Loading
| Success a
| Failure e
type alias Model =
{ user : RemoteData Http.Error User
}
init : () -> ( Model, Cmd Msg )
init _ =
( { user = NotAsked }, Cmd.none )
-- UPDATE
type Msg
= FetchUser
| GotUser (Result Http.Error User)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FetchUser ->
( { model | user = Loading }
, fetchUser 1
)
GotUser result ->
case result of
Ok user ->
( { model | user = Success user }
, Cmd.none
)
Err error ->
( { model | user = Failure error }
, Cmd.none
)
-- HTTP
fetchUser : Int -> Cmd Msg
fetchUser userId =
Http.get
{ url = "https://api.example.com/users/" ++ String.fromInt userId
, expect = Http.expectJson GotUser userDecoder
}
userDecoder : Decoder User
userDecoder =
Decode.map3 User
(Decode.field "id" Decode.int)
(Decode.field "name" Decode.string)
(Decode.field "email" Decode.string)
-- VIEW
view : Model -> Html Msg
view model =
div []
[ case model.user of
NotAsked ->
button [ onClick FetchUser ] [ text "Fetch User" ]
Loading ->
text "Loading..."
Success user ->
div []
[ text ("User: " ++ user.name ++ " (" ++ user.email ++ ")")
]
Failure error ->
text ("Error: " ++ httpErrorToString error)
]
httpErrorToString : Http.Error -> String
httpErrorToString error =
case error of
Http.BadUrl url ->
"Bad URL: " ++ url
Http.Timeout ->
"Timeout"
Http.NetworkError ->
"Network error"
Http.BadStatus status ->
"Bad status: " ++ String.fromInt status
Http.BadBody body ->
"Bad body: " ++ body
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
Key changes:
! operator → Elm's Cmd and Msg handlingFor more examples and patterns, see:
meta-convert-dev - Foundational patterns with cross-language examplesconvert-erlang-elm - Similar backend-to-frontend conversion patternslang-roc-dev - Roc development patternslang-elm-dev - Elm development patternsCross-cutting pattern skills:
patterns-concurrency-dev - Task vs Cmd/Sub comparisonpatterns-serialization-dev - JSON encoding/decoding across languagesxiaohongshu
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