| 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: Result error ok
divide : Int -> Int -> Result String Int
# 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
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
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:
-- Named custom type (nominal)
type Color
= Red
| Green
| Blue
| Custom Int Int Int
type alias RGB =
{ r : Int, g : Int, b : Int }
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
typedeclaration - Roc uses structural types (no declaration needed)
- Elm uses type constructors with
| - Roc uses tag union syntax with
[]
Optional Values
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:
# 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:
-- 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:
# 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 okhas error first - Roc's
Result ok errhas success first - Always reverse the parameter order when converting
Idiom Translation
1. Pattern Matching: case → when
Elm:
classify : Int -> String
classify n =
case n of
0 ->
"zero"
x ->
if x < 0 then
"negative"
else
"positive"
Roc:
classify : I64 -> Str
classify = \n ->
when n is
0 -> "zero"
x if x < 0 -> "negative"
_ -> "positive"
Why this translation:
- Elm's
casebecomes Roc'swhen - Elm uses
ifexpressions in branches, Roc has guard clauses (ifafter pattern) - Roc allows inline guards which are more concise
2. List Processing
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:
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)andList.map list fnsyntax
3. Record Updates
Elm:
user =
{ name = "Alice", age = 30 }
olderUser =
{ user | age = 31 }
-- Multiple fields
updatedUser =
{ user
| age = 31
, name = "Alice Smith"
}
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:
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:
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.performbecomes 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:
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: 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.andThenbecomes 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 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:
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'sAge n - Roc uses
Resultfor validation, Elm usesMaybe(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):
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):
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: 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: 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:
type FetchError
= NetworkError Http.Error
| NotFound
| Unauthorized
fetchUser : Int -> Task FetchError User
Roc uses inline tag unions:
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):
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):
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:
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:
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:
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→interfaceexposing→exposestype alias→ type annotation- Same visibility model (only exposed items are public)
Import Patterns
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:
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
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!
- Elm:
Record update syntax
- Elm:
{ record | field = value } - Roc:
{ record & field: value } - Don't mix up
|/&and=/:
- Elm:
Case vs When syntax
- Elm:
case x of - Roc:
when x is - Remember
isnotof
- Elm:
Platform target mismatch
- Elm targets browser only (DOM, HTML, CSS)
- Roc is platform-agnostic (CLI, native, potentially web)
- Browser-specific Elm code needs redesign
Bang operator vs explicit Task
- Elm: No
!operator, useTask.performorCmd - Roc:
value = task!for sequential execution - Much more concise in Roc
- Elm: No
Capitalization
- Elm:
True,False - Roc:
Bool.true,Bool.false - Watch for True/False differences
- Elm:
Function types
- Elm:
a -> b -> c(curried) - Roc:
a, b -> c(comma-separated params) - Roc allows both, but commas are clearer
- Elm:
Maybe vs tag union
- Elm: Built-in
Maybe awithJust/Nothing - Roc: Use
[Some a, None](no built-in Maybe) - Must define tag union explicitly
- Elm: Built-in
List function names
- Elm:
List.filter,List.foldl,List.foldr - Roc:
List.keepIf,List.walk - Same concepts, different names
- Elm:
String interpolation
- Elm:
"Hello, " ++ name ++ "!" - Roc:
"Hello, \(name)!" - Roc has built-in string interpolation
- Elm:
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):
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):
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):
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):
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
typedeclaration → Structural tag union case x of→when x isInt→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):
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):
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 Msghandling → 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'sResult 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:
-- 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:
# 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
describeandtest→ Roc's flatexpectstatements - Elm's
Expect.equal→ Roc's==operator - Run with
elm-test→ Run withroc test
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language examplesconvert-roc-elm- Reverse conversion (Roc → Elm)convert-elm-haskell- Similar functional language conversion patternslang-elm-dev- Elm development patternslang-roc-dev- Roc development patterns
Cross-cutting pattern skills:
patterns-concurrency-dev- Cmd/Sub vs Task comparisonpatterns-serialization-dev- JSON encoding/decoding across languagespatterns-metaprogramming-dev- Why both languages avoid metaprogramming