Agents convert-elm-roc
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.
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/convert-elm-roc" ~/.claude/skills/arustydev-agents-convert-elm-roc && rm -rf "$T"
content/skills/convert-elm-roc/SKILL.mdElm ↔ 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
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
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 |
|---|---|---|
| | Direct mapping |
| or | Choose signed/unsigned based on domain |
| | Direct mapping |
| | Same with capitalization |
| | Same syntax and operations |
| | Records are nearly identical |
| | Custom types → Tag unions |
| | REVERSED parameter order! |
| | Optional values |
or | | Effect systems differ |
| | Pattern matching syntax |
| 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:
- Error type first, success type secondResult error ok - Roc:
- Success type first, error type secondResult ok err
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 | | |
| State | Explicit Model | Implicit in Task chain |
| Updates | | 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 |
|---|---|---|
/ | / | Capitalization differs |
| | Integer literals |
| | Float literals |
| | String literals |
| or | Elm has arbitrary precision, Roc has sized types |
| or | Elm has single Float, Roc has sized types |
| | Direct mapping |
| | Roc treats chars as Unicode scalar values |
Collection Types
| Elm | Roc | Notes |
|---|---|---|
| | Identical syntax and semantics |
| | Roc requires to implement Hash & Eq |
| | Roc requires to implement Hash & Eq |
| | Tuples (Roc supports arbitrary tuple sizes) |
| | Elm's Array → Roc's List (Roc optimizes internally) |
Record Types
| Elm | Roc | Notes |
|---|---|---|
| | Nearly identical, just type name differences |
| | Record update syntax differs (| vs &, = vs :) |
| | Destructuring identical |
| | 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
declarationtype - 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 valueSome(value)
→NothingNone
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
has error firstResult error ok - Roc's
has success firstResult ok err - 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
becomes Roc'scasewhen - Elm uses
expressions in branches, Roc has guard clauses (if
after pattern)if - 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
(different name, same semantics)List.keepIf
/List.foldl
→List.foldr
(different name)List.walk- Pipeline operator
is identical|> - Roc supports both
andList.map(list, fn)
syntaxList.map list fn
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
becomes Roc'sTask.perform
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
becomes Roc'sResult.andThen
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
wrapping is more explicit than Elm's@Age(n)Age n - Roc uses
for validation, Elm usesResult
(different conventions)Maybe
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 |
returns Program | 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:
→moduleinterface
→exposingexposes
→ type annotationtype alias- 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:
→exposingexposing- Parentheses
→ Brackets()[] - Elm's
→ Not yet available in Rocimport as
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
notisof
- 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, use!
orTask.performCmd - Roc:
for sequential executionvalue = task! - Much more concise in Roc
- Elm: No
-
Capitalization
- Elm:
,TrueFalse - Roc:
,Bool.trueBool.false - Watch for True/False differences
- Elm:
-
Function types
- Elm:
(curried)a -> b -> c - Roc:
(comma-separated params)a, b -> c - Roc allows both, but commas are clearer
- Elm:
-
Maybe vs tag union
- Elm: Built-in
withMaybe a
/JustNothing - Roc: Use
(no built-in Maybe)[Some a, None] - Must define tag union explicitly
- Elm: Built-in
-
List function names
- Elm:
,List.filter
,List.foldlList.foldr - Roc:
,List.keepIfList.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 | | | Both enforce standard style |
| REPL | | | Both support interactive testing |
| Test | | | Different syntax (case vs expect) |
| Build | | | Elm → JavaScript, Roc → native |
| Package manager | | Platform URLs | Roc uses URL-based dependencies |
| Linter | | 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
→IntU32- String concatenation
→ interpolation++\(...)
→String.fromIntNum.toStr- Elm's separate test file → inline
expect
→ type annotationtype alias
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
declaration → Structural tag uniontype
→case x ofwhen x is
→Int
(Roc has sized integers)U8- String concatenation → interpolation
- Constructor
→Custom r g bCustom(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
handling → Roc'sCmd Msg
operator! - Elm's HTML view → Roc's CLI output
- Elm's loading states → Roc's direct execution
- Elm's
→ Roc'sResult Http.Error User
(reversed params!)Result User [HttpErr, DecodeErr] - 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
anddescribe
→ Roc's flattest
statementsexpect - Elm's
→ Roc'sExpect.equal
operator== - Run with
→ Run withelm-testroc test
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Similar functional language conversion patternsconvert-elm-haskell
- Elm development patternslang-elm-dev
- Roc development patternslang-roc-dev
Cross-cutting pattern skills:
- Cmd/Sub vs Task comparisonpatterns-concurrency-dev
- JSON encoding/decoding across languagespatterns-serialization-dev
- Why both languages avoid metaprogrammingpatterns-metaprogramming-dev