Convert Elm code to idiomatic Roc. 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 2/12/2026
Install via CLI
openskills install aRustyDev/ai---
name: convert-elm-roc
description: Convert Elm code to idiomatic Roc. 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.
---
# Convert Elm to Roc
Convert Elm code to idiomatic Roc. This skill extends `meta-convert-dev` with Elm-to-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`
- Reverse conversion (Roc → Elm) - see `convert-roc-elm`
- 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-roc-elm` - Reverse conversion (Roc → Elm)
- `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
No comments yet. Be the first to comment!