Claude-skill-registry convert-elm-roc

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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convert-elm-roc" ~/.claude/skills/majiayu000-claude-skill-registry-convert-elm-roc && rm -rf "$T"
manifest: skills/data/convert-elm-roc/SKILL.md
source content

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

ElmRocNotes
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

AspectElm TEARoc Platform Model
TargetBrowser frontend onlyAny platform (CLI, web, native)
EffectsRuntime-managed Cmd/SubPlatform-provided Task
Entry point
main : Program () Model Msg
main : Task {} []
StateExplicit ModelImplicit in Task chain
Updates
update : Msg → Model → (Model, Cmd Msg)
Task composition
I/OBrowser.* modules onlyPlatform 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

ElmRocNotes
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

ElmRocNotes
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

ElmRocNotes
{ 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
    type
    declaration
  • 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 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:

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
    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:

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)
    and
    List.map list fn
    syntax

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.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:

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.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 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's
    Age n
  • Roc uses
    Result
    for validation, Elm uses
    Maybe
    (different conventions)

Paradigm Translation: TEA → Platform Model

Mental Model Shift

Elm ConceptRoc ApproachKey Insight
Model-Update-View loopTask chain (sequential)Declarative → Imperative
Browser provides eventsPlatform provides I/OBrowser → CLI/Native
main
returns Program
main
returns Task
Pure → Effect
Cmd issued, Msg receivedTasks compose with
!
Indirect → Direct

Effect System Comparison

Elm ModelRoc ModelConceptual Translation
Runtime handles Cmd/SubPlatform handles TasksBoth managed by runtime
Asynchronous with MsgSequential with
!
Event-driven → Chain
Cmd.none / new CmdTask.ok/Task.errSide 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
    interface
  • exposing
    exposes
  • type 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

  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

ToolElmRocNotes
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 URLsRoc uses URL-based dependencies
Linter
elm-review
N/AElm 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
    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):

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 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:

-- 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
    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