Claude Code Plugins

Community-maintained marketplace

Feedback

convert-elm-roc

@aRustyDev/ai
0
0

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 Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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 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 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 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.filterList.keepIf (different name, same semantics)
  • List.foldl / List.foldrList.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 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:

  • moduleinterface
  • exposingexposes
  • 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:

  • exposingexposing
  • 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):

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:

  • StringStr, IntU32
  • String concatenation ++ → interpolation \(...)
  • String.fromIntNum.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 ofwhen x is
  • IntU8 (Roc has sized integers)
  • 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 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