Bidirectional conversion between Elm and Roc. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Elm↔Roc specific patterns. Use when migrating Elm frontend code to Roc applications, translating browser-based Elm to platform-agnostic Roc, or refactoring Elm web applications to Roc CLI/native tools. Extends meta-convert-dev with Elm-to-Roc specific patterns.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add aRustyDev/agents --skill convert-elm-roc --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Convert Elm Roc?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/arustydev-convert-elm-roc-agents)More formats (shields.io, HTML) on the badges page.
---
name: convert-elm-roc
description: Bidirectional conversion between Elm and Roc. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Elm↔Roc specific patterns. Use when migrating Elm frontend code to Roc applications, translating browser-based Elm to platform-agnostic Roc, or refactoring Elm web applications to Roc CLI/native tools. Extends meta-convert-dev with Elm-to-Roc specific patterns.
---
# Elm ↔ Roc Conversion
Bidirectional conversion between Elm and Roc. This skill extends `meta-convert-dev` with Elm↔Roc specific type mappings, idiom translations, and architectural patterns for moving from browser-based Elm applications to platform-agnostic Roc code.
## 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**: Elm types → Roc types
- **Idiom translations**: Elm patterns → idiomatic Roc
- **Architecture patterns**: The Elm Architecture (TEA) → Platform model
- **Effect system**: Cmd/Sub → Task
- **Error handling**: Result types (**REVERSED parameter order!**)
- **Platform shift**: Frontend-specific → General-purpose
## This Skill Does NOT Cover
- General conversion methodology - see `meta-convert-dev`
- Elm language fundamentals - see `lang-elm-dev`
- Roc language fundamentals - see `lang-roc-dev`
- Browser-specific Elm code - Roc doesn't have DOM access
---
## Quick Reference
| Elm | Roc | Notes |
|-----|-----|-------|
| `String` | `Str` | Direct mapping |
| `Int` | `I64` or `U64` | Choose signed/unsigned based on domain |
| `Float` | `F64` | Direct mapping |
| `Bool` | `Bool` | Same with capitalization |
| `List a` | `List a` | Same syntax and operations |
| `{ field : Type }` | `{ field : Type }` | Records are nearly identical |
| `type Custom = Tag1 \| Tag2` | `[Tag1, Tag2]` | Custom types → Tag unions |
| `Result err ok` | `Result ok err` | **REVERSED parameter order!** |
| `Maybe a` | `[Some a, None]` | Optional values |
| `Cmd Msg` or `Task err a` | `Task ok err` | Effect systems differ |
| `case x of` | `when x is` | Pattern matching syntax |
| `Task.perform` | `!` suffix operator | Explicit handling → Bang operator |
---
## 🚨 CRITICAL GOTCHA: Result Type Parameter Order
**This is the most important thing to remember when converting Elm to Roc:**
```elm
-- Elm: Result error ok
divide : Int -> Int -> Result String Int
```
```roc
# Roc: Result ok err (REVERSED!)
divide : I64, I64 -> Result I64 Str
```
### Why This Matters
The parameter order is **completely reversed** between Elm and Roc:
- **Elm**: `Result error ok` - Error type first, success type second
- **Roc**: `Result ok err` - Success type first, error type second
This affects:
- Type signatures
- Type annotations
- Generic type parameters
- Error handling patterns
### Always Remember
When you see Elm's `Result String User`, it becomes Roc's `Result User Str`.
❌ **Wrong**: `Result Str User` (copying Elm order)
✓ **Correct**: `Result User Str` (reversed order)
---
## Architectural Paradigm Shift
### From The Elm Architecture to Platform Model
| Aspect | Elm TEA | Roc Platform Model |
|--------|---------|-------------------|
| **Target** | Browser frontend only | Any platform (CLI, web, native) |
| **Effects** | Runtime-managed Cmd/Sub | Platform-provided Task |
| **Entry point** | `main : Program () Model Msg` | `main : Task {} []` |
| **State** | Explicit Model | Implicit in Task chain |
| **Updates** | `update : Msg → Model → (Model, Cmd Msg)` | Task composition |
| **I/O** | Browser.* modules only | Platform exposes (File, Http, etc.) |
### Elm TEA Application
```elm
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
}
```
### Roc Platform Equivalent
```roc
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br"
}
import pf.Stdout
import pf.Stdin
import pf.Task exposing [Task]
main : Task {} []
main =
Stdout.line! "Hello, World!"
Stdout.line! "Enter your name:"
name = Stdin.line!
Stdout.line! "Hello, \(name)!"
```
**Key shift:** Elm's declarative Model-Update-View loop becomes Roc's imperative Task chain.
**Note:** The Roc version is CLI-based because Roc doesn't target the browser. For equivalent browser functionality, you'd need a Roc web platform (still in development).
---
## Type System Mapping
### Primitive Types
| Elm | Roc | Notes |
|-----|-----|-------|
| `True` / `False` | `Bool.true` / `Bool.false` | Capitalization differs |
| `42` | `42` | Integer literals |
| `3.14` | `3.14` | Float literals |
| `"text"` | `"text"` | String literals |
| `Int` | `I64` or `U64` | Elm has arbitrary precision, Roc has sized types |
| `Float` | `F64` or `F32` | Elm has single Float, Roc has sized types |
| `String` | `Str` | Direct mapping |
| `Char` | `U32` | Roc treats chars as Unicode scalar values |
### Collection Types
| Elm | Roc | Notes |
|-----|-----|-------|
| `List a` | `List a` | Identical syntax and semantics |
| `Dict comparable v` | `Dict k v` | Roc requires `k` to implement Hash & Eq |
| `Set comparable` | `Set a` | Roc requires `a` to implement Hash & Eq |
| `( a, b )` | `(a, b)` | Tuples (Roc supports arbitrary tuple sizes) |
| `Array a` | `List a` | Elm's Array → Roc's List (Roc optimizes internally) |
### Record Types
| Elm | Roc | Notes |
|-----|-----|-------|
| `{ name : String, age : Int }` | `{ name : Str, age : U32 }` | Nearly identical, just type name differences |
| `{ user \| age = 31 }` | `{ user & age: 31 }` | Record update syntax differs (\| vs &, = vs :) |
| `{ name, age } = user` | `{ name, age } = user` | Destructuring identical |
| `user.name` | `user.name` | Field access identical |
### Custom Types to Tag Unions
**Elm:**
```elm
-- Named custom type (nominal)
type Color
= Red
| Green
| Blue
| Custom Int Int Int
type alias RGB =
{ r : Int, g : Int, b : Int }
```
**Roc:**
```roc
# Structural tag union
Color : [Red, Green, Blue, Custom(U8, U8, U8)]
# Record type alias
RGB : { r : U8, g : U8, b : U8 }
```
**Key differences:**
- Elm requires explicit `type` declaration
- Roc uses structural types (no declaration needed)
- Elm uses type constructors with `|`
- Roc uses tag union syntax with `[]`
### Optional Values
**Elm:**
```elm
-- Built-in Maybe type
email : Maybe String
email = Just "alice@example.com"
-- Pattern match
emailText : String
emailText =
case email of
Just addr ->
addr
Nothing ->
"no email"
-- Helper functions
emailOrDefault : String
emailOrDefault =
Maybe.withDefault "no email" email
```
**Roc:**
```roc
# Inline tag union (no built-in Maybe)
email : [Some Str, None]
email = Some("alice@example.com")
# Pattern match
emailText : Str
emailText =
when email is
Some(addr) -> addr
None -> "no email"
# Manual helper or use Result
```
**Translation:**
- `Maybe a` → `[Some a, None]`
- `Just value` → `Some(value)`
- `Nothing` → `None`
### Result Type (Parameter Order Reversed!)
**Elm:**
```elm
-- Result error ok
divide : Int -> Int -> Result String Int
divide a b =
if b == 0 then
Err "Division by zero"
else
Ok (a // b)
```
**Roc:**
```roc
# Result ok err (REVERSED!)
divide : I64, I64 -> Result I64 Str
divide = \a, b ->
if b == 0 then
Err("Division by zero")
else
Ok(a // b)
```
**CRITICAL:**
- Elm's `Result error ok` has error first
- Roc's `Result ok err` has success first
- **Always reverse the parameter order** when converting
---
## Idiom Translation
### 1. Pattern Matching: case → when
**Elm:**
```elm
classify : Int -> String
classify n =
case n of
0 ->
"zero"
x ->
if x < 0 then
"negative"
else
"positive"
```
**Roc:**
```roc
classify : I64 -> Str
classify = \n ->
when n is
0 -> "zero"
x if x < 0 -> "negative"
_ -> "positive"
```
**Why this translation:**
- Elm's `case` becomes Roc's `when`
- Elm uses `if` expressions in branches, Roc has guard clauses (`if` after pattern)
- Roc allows inline guards which are more concise
### 2. List Processing
**Elm:**
```elm
doubled : List Int
doubled =
List.map (\x -> x * 2) [ 1, 2, 3, 4, 5 ]
-- Pipeline style
result : Int
result =
[ 1, 2, 3, 4, 5 ]
|> List.map (\x -> x * 2)
|> List.filter (\x -> x > 5)
|> List.foldl (+) 0
```
**Roc:**
```roc
doubled : List I64
doubled = List.map([1, 2, 3, 4, 5], \x -> x * 2)
# Pipeline style (same!)
result : I64
result = [1, 2, 3, 4, 5]
|> List.map(\x -> x * 2)
|> List.keepIf(\x -> x > 5)
|> List.walk(0, Num.add)
```
**Why this translation:**
- `List.filter` → `List.keepIf` (different name, same semantics)
- `List.foldl` / `List.foldr` → `List.walk` (different name)
- Pipeline operator `|>` is identical
- Roc supports both `List.map(list, fn)` and `List.map list fn` syntax
### 3. Record Updates
**Elm:**
```elm
user =
{ name = "Alice", age = 30 }
olderUser =
{ user | age = 31 }
-- Multiple fields
updatedUser =
{ user
| age = 31
, name = "Alice Smith"
}
```
**Roc:**
```roc
user = { name: "Alice", age: 30 }
olderUser = { user & age: 31 }
# Multiple fields
updatedUser = { user &
age: 31,
name: "Alice Smith"
}
```
**Why this translation:**
- Elm uses `|` for updates, Roc uses `&`
- Elm uses `=` for field assignment, Roc uses `:`
- Syntax is almost identical otherwise
### 4. Cmd/Task → Task
**Elm:**
```elm
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
)
```
**Roc:**
```roc
main : Task {} []
main =
data = Http.get!("https://api.example.com/data")
processed = Str.toUpper(data)
Task.ok({})
```
**Why this translation:**
- Elm's Cmd with Msg handling becomes Roc's direct Task chaining
- Elm's `Task.perform` becomes Roc's `!` operator
- Elm's event-driven model becomes Roc's sequential execution
- No Model or Msg types needed in Roc for simple cases
### 5. Error Propagation
**Elm:**
```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
```
**Roc:**
```roc
# Roc: Result ok err (reversed params!)
calculate : I64, I64, I64 -> Result I64 Str
calculate = \a, b, c ->
x = divide!(a, b) # Returns early on Err
y = divide!(x, c) # Returns early on Err
Ok(y)
```
**Why this translation:**
- Elm's `Result.andThen` becomes Roc's `!` operator
- Roc's `!` provides automatic early return on error
- Much more concise than Elm's explicit chaining
- Remember to **reverse Result type parameters**!
### 6. Opaque Types
**Elm:**
```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
```
**Roc:**
```roc
interface Age
exposes [Age, create, toU32]
imports []
# Opaque type
Age := U32
create : U32 -> Result Age [InvalidAge]
create = \n ->
if n >= 0 && n < 150 then
Ok(@Age(n))
else
Err(InvalidAge)
toU32 : Age -> U32
toU32 = \@Age(n) -> n
```
**Why this translation:**
- Elm uses pattern matching for unwrapping, Roc uses `@` syntax
- Both achieve opacity through module exports
- Roc's `@Age(n)` wrapping is more explicit than Elm's `Age n`
- Roc uses `Result` for validation, Elm uses `Maybe` (different conventions)
---
## Paradigm Translation: TEA → Platform Model
### Mental Model Shift
| Elm Concept | Roc Approach | Key Insight |
|-------------|--------------|-------------|
| Model-Update-View loop | Task chain (sequential) | Declarative → Imperative |
| Browser provides events | Platform provides I/O | Browser → CLI/Native |
| `main` returns Program | `main` returns Task | Pure → Effect |
| Cmd issued, Msg received | Tasks compose with `!` | Indirect → Direct |
### Effect System Comparison
| Elm Model | Roc Model | Conceptual Translation |
|-----------|-----------|------------------------|
| Runtime handles Cmd/Sub | Platform handles Tasks | Both managed by runtime |
| Asynchronous with Msg | Sequential with `!` | Event-driven → Chain |
| Cmd.none / new Cmd | Task.ok/Task.err | Side effect → Return value |
### Example: HTTP Fetch
**Elm (TEA):**
```elm
type alias Model =
{ users : RemoteData Http.Error (List User)
}
type Msg
= FetchUsers
| GotUsers (Result Http.Error (List User))
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FetchUsers ->
( { model | users = Loading }
, Http.get
{ url = "/api/users"
, expect = Http.expectJson GotUsers usersDecoder
}
)
GotUsers result ->
case result of
Ok users ->
( { model | users = Success users }, Cmd.none )
Err error ->
( { model | users = Failure error }, Cmd.none )
```
**Roc (Platform):**
```roc
main : Task {} []
main =
users = Http.get!("/api/users")
decoded = Decode.fromBytes!(users, usersDecoder)
Stdout.line!("Fetched \(List.len(decoded) |> Num.toStr) users")
```
**Key differences:**
- Elm models loading states explicitly
- Roc handles success/error sequentially
- Elm's async becomes Roc's sequential (platform handles concurrency)
- No Model or Msg types needed in Roc
---
## Error Handling
### Elm Result → Roc Result
**Key Difference: Parameter order is reversed!**
```elm
-- Elm: Result err ok
parseAge : String -> Result String Int
parseAge str =
case String.toInt str of
Just age ->
if age >= 0 then
Ok age
else
Err "Negative age"
Nothing ->
Err "Not a number"
```
```roc
# Roc: Result ok err (REVERSED!)
parseAge : Str -> Result U32 Str
parseAge = \str ->
when Str.toU32(str) is
Ok(age) if age >= 0 -> Ok(age)
Ok(_) -> Err("Negative age")
Err(_) -> Err("Not a number")
```
### Error Type Modeling
**Elm uses named custom types:**
```elm
type FetchError
= NetworkError Http.Error
| NotFound
| Unauthorized
fetchUser : Int -> Task FetchError User
```
**Roc uses inline tag unions:**
```roc
fetchUser : U64 -> Task User [NetworkError, NotFound, Unauthorized]
```
**Translation:**
- Elm's named error types → Roc's inline tag unions
- Same expressiveness, less ceremony
- Roc is structural, Elm is nominal
---
## Effect System Translation
### Cmd in Elm vs Task in Roc
**Elm Cmd (event-driven):**
```elm
type Msg
= GotData (Result Http.Error String)
fetchData : Cmd Msg
fetchData =
Http.get
{ url = "https://api.example.com/data"
, expect = Http.expectString GotData
}
-- Must handle result in update function
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotData result ->
-- Handle result here
...
```
**Roc Task (sequential):**
```roc
fetchData : Task Str []
fetchData =
Http.get!("https://api.example.com/data")
```
**Why this translation:**
- Elm's Cmd is fire-and-forget, result comes via Msg
- Roc's Task chains sequentially with `!`
- Elm separates effect from handling, Roc combines them
### Sub in Elm vs Task in Roc
**Elm Subscriptions:**
```elm
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Time.every 1000 Tick
, Browser.Events.onResize WindowResized
]
```
**Roc approach:**
Roc doesn't have built-in subscriptions. Platforms may provide equivalent mechanisms through Task-based polling or event streams, but this is platform-specific.
For periodic tasks, you'd typically use platform-specific APIs or structure your `main` Task to loop.
---
## Module System Translation
### Elm Modules → Roc Interfaces
**Elm:**
```elm
module User exposing (User, create, getName, getAge)
type alias User =
{ name : String
, age : Int
}
create : String -> Int -> User
create name age =
{ name = name, age = age }
getName : User -> String
getName user =
user.name
getAge : User -> Int
getAge user =
user.age
```
**Roc:**
```roc
interface User
exposes [User, create, getName, getAge]
imports []
User : {
name : Str,
age : U32,
}
create : Str, U32 -> User
create = \name, age ->
{ name, age }
getName : User -> Str
getName = \user -> user.name
getAge : User -> U32
getAge = \user -> user.age
```
**Translation:**
- `module` → `interface`
- `exposing` → `exposes`
- `type alias` → type annotation
- Same visibility model (only exposed items are public)
### Import Patterns
**Elm:**
```elm
import Dict
import Dict exposing (Dict)
import List exposing (map, filter)
import Maybe exposing (Maybe(..))
import Html as H
import Html.Events as Events
```
**Roc:**
```roc
import Dict
import Dict exposing [Dict]
import List exposing [map, keepIf]
import pf.Stdout
import pf.Task exposing [Task]
# Note: Roc doesn't have import aliasing yet
# Must use full qualified names
```
**Translation:**
- `exposing` → `exposing`
- Parentheses `()` → Brackets `[]`
- Elm's `import as` → Not yet available in Roc
---
## Common Pitfalls
1. **Result parameter order reversal (MOST CRITICAL)**
- Elm: `Result err ok`
- Roc: `Result ok err`
- **Always reverse parameters** when converting Result types
- Double-check every Result type signature!
2. **Record update syntax**
- Elm: `{ record | field = value }`
- Roc: `{ record & field: value }`
- Don't mix up `|`/`&` and `=`/`:`
3. **Case vs When syntax**
- Elm: `case x of`
- Roc: `when x is`
- Remember `is` not `of`
4. **Platform target mismatch**
- Elm targets browser only (DOM, HTML, CSS)
- Roc is platform-agnostic (CLI, native, potentially web)
- Browser-specific Elm code needs redesign
5. **Bang operator vs explicit Task**
- Elm: No `!` operator, use `Task.perform` or `Cmd`
- Roc: `value = task!` for sequential execution
- Much more concise in Roc
6. **Capitalization**
- Elm: `True`, `False`
- Roc: `Bool.true`, `Bool.false`
- Watch for True/False differences
7. **Function types**
- Elm: `a -> b -> c` (curried)
- Roc: `a, b -> c` (comma-separated params)
- Roc allows both, but commas are clearer
8. **Maybe vs tag union**
- Elm: Built-in `Maybe a` with `Just`/`Nothing`
- Roc: Use `[Some a, None]` (no built-in Maybe)
- Must define tag union explicitly
9. **List function names**
- Elm: `List.filter`, `List.foldl`, `List.foldr`
- Roc: `List.keepIf`, `List.walk`
- Same concepts, different names
10. **String interpolation**
- Elm: `"Hello, " ++ name ++ "!"`
- Roc: `"Hello, \(name)!"`
- Roc has built-in string interpolation
---
## Tooling
| Tool | Elm | Roc | Notes |
|------|-----|-----|-------|
| Formatter | `elm-format` | `roc format` | Both enforce standard style |
| REPL | `elm repl` | `roc repl` | Both support interactive testing |
| Test | `elm-test` | `roc test` | Different syntax (case vs expect) |
| Build | `elm make` | `roc build` | Elm → JavaScript, Roc → native |
| Package manager | `elm install` | Platform URLs | Roc uses URL-based dependencies |
| Linter | `elm-review` | N/A | Elm has rich linting, Roc doesn't yet |
---
## Examples
### Example 1: Simple - Type and Function Translation
**Before (Elm):**
```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
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."
```
**After (Roc):**
```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."
```
**Key changes:**
- `String` → `Str`, `Int` → `U32`
- String concatenation `++` → interpolation `\(...)`
- `String.fromInt` → `Num.toStr`
- Elm's separate test file → inline `expect`
- `type alias` → type annotation
### Example 2: Medium - Custom Types and Pattern Matching
**Before (Elm):**
```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
String.fromInt n -- Simplified
```
**After (Roc):**
```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
Num.toStr(n) # Simplified
```
**Key changes:**
- Named `type` declaration → Structural tag union
- `case x of` → `when x is`
- `Int` → `U8` (Roc has sized integers)
- String concatenation → interpolation
- Constructor `Custom r g b` → `Custom(r, g, b)`
### Example 3: Complex - TEA to Platform Model
**Before (Elm):**
```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
}
```
**After (Roc):**
```roc
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br"
}
import pf.Http
import pf.Stdout
import pf.Task exposing [Task]
import json.Decode
# Note: Result type parameters REVERSED!
# Elm: Result Http.Error User
# Roc: Result User [HttpErr]
User : { id : U64, name : Str, email : Str }
fetchUser : U64 -> Task User [HttpErr, DecodeErr]
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(err) -> Task.err(DecodeErr)
userDecoder : Decode.Decoder User
userDecoder =
Decode.record(\field ->
{
id: field.required("id", Decode.u64),
name: field.required("name", Decode.str),
email: field.required("email", Decode.str),
}
)
main : Task {} []
main =
when fetchUser(1) is
Ok(user) ->
Stdout.line!("User: \(user.name) (\(user.email))")
Err(HttpErr) ->
Stdout.line!("HTTP error occurred")
Err(DecodeErr) ->
Stdout.line!("Failed to decode user")
```
**Key changes:**
- Elm's TEA (Model-Update-View) → Roc's Task chain
- Elm's `Cmd Msg` handling → Roc's `!` operator
- Elm's HTML view → Roc's CLI output
- Elm's loading states → Roc's direct execution
- Elm's `Result Http.Error User` → Roc's `Result User [HttpErr, DecodeErr]` (**reversed params!**)
- No Model, Msg, or update function needed
- Direct error handling with pattern matching
---
## Testing Translation
### Elm's elm-test → Roc's expect
**Elm:**
```elm
-- tests/UserTests.elm
module UserTests exposing (suite)
import Test exposing (Test, describe, test)
import Expect
import User
suite : Test
suite =
describe "User module"
[ describe "greet"
[ test "formats greeting correctly" <|
\_ ->
User.greet { name = "Alice", age = 30 }
|> Expect.equal "Hello, Alice! You are 30 years old."
, test "handles young age" <|
\_ ->
User.greet { name = "Bob", age = 5 }
|> Expect.equal "Hello, Bob! You are 5 years old."
]
]
```
**Roc:**
```roc
# User.roc
interface User
exposes [User, greet]
imports []
User : { name : Str, age : U32 }
greet : User -> Str
greet = \user ->
"Hello, \(user.name)! You are \(Num.toStr(user.age)) years old."
# Inline tests
expect greet({ name: "Alice", age: 30 }) == "Hello, Alice! You are 30 years old."
expect greet({ name: "Bob", age: 5 }) == "Hello, Bob! You are 5 years old."
```
**Translation:**
- Elm's separate test files → Roc's inline `expect`
- Elm's `describe` and `test` → Roc's flat `expect` statements
- Elm's `Expect.equal` → Roc's `==` operator
- Run with `elm-test` → Run with `roc test`
---
## See Also
For more examples and patterns, see:
- `meta-convert-dev` - Foundational patterns with cross-language examples
- `convert-elm-haskell` - Similar functional language conversion patterns
- `lang-elm-dev` - Elm development patterns
- `lang-roc-dev` - Roc development patterns
Cross-cutting pattern skills:
- `patterns-concurrency-dev` - Cmd/Sub vs Task comparison
- `patterns-serialization-dev` - JSON encoding/decoding across languages
- `patterns-metaprogramming-dev` - Why both languages avoid metaprogramming
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!