Skip to content
Back to skills

Convert Elm Roc

ASecurity

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.

  • 8 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added February 8, 2026
developmentjavascriptgojavaexpresstestingrefactoringgitapifrontend

Works with

  • cli
  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add aRustyDev/ai --skill convert-elm-roc --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Convert Elm Roc?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Convert Elm Roc
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/arustydev-convert-elm-roc/badge)](https://www.skillsdirectory.com/skills/arustydev-convert-elm-roc)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
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
-- Elm: Result error ok
divide : Int -> Int -> Result String Int
```

```roc
# Roc: Result ok err (REVERSED!)
divide : I64, I64 -> Result I64 Str
```

### Why This Matters

The parameter order is **completely reversed** between Elm and Roc:

- **Elm**: `Result error ok` - Error type first, success type second
- **Roc**: `Result ok err` - Success type first, error type second

This affects:
- Type signatures
- Type annotations
- Generic type parameters
- Error handling patterns

### Always Remember

When you see Elm's `Result String User`, it becomes Roc's `Result User Str`.

❌ **Wrong**: `Result Str User` (copying Elm order)
✓ **Correct**: `Result User Str` (reversed order)

---

## Architectural Paradigm Shift

### From The Elm Architecture to Platform Model

| Aspect | Elm TEA | Roc Platform Model |
|--------|---------|-------------------|
| **Target** | Browser frontend only | Any platform (CLI, web, native) |
| **Effects** | Runtime-managed Cmd/Sub | Platform-provided Task |
| **Entry point** | `main : Program () Model Msg` | `main : Task {} []` |
| **State** | Explicit Model | Implicit in Task chain |
| **Updates** | `update : Msg → Model → (Model, Cmd Msg)` | Task composition |
| **I/O** | Browser.* modules only | Platform exposes (File, Http, etc.) |

### Elm TEA Application

```elm
module Main exposing (main)

import Browser
import Html exposing (Html, div, input, text)
import Html.Events exposing (onInput)
import Html.Attributes exposing (placeholder, value)

-- MODEL
type alias Model =
    { name : String
    , greeting : String
    }

init : () -> ( Model, Cmd Msg )
init _ =
    ( { name = "", greeting = "Hello, World!" }, Cmd.none )

-- UPDATE
type Msg
    = NameChanged String

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        NameChanged newName ->
            ( { model
                | name = newName
                , greeting = "Hello, " ++ newName ++ "!"
              }
            , Cmd.none
            )

-- VIEW
view : Model -> Html Msg
view model =
    div []
        [ div [] [ text model.greeting ]
        , input
            [ placeholder "Enter your name"
            , value model.name
            , onInput NameChanged
            ]
            []
        ]

-- MAIN
main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , view = view
        , subscriptions = \_ -> Sub.none
        }
```

### Roc Platform Equivalent

```roc
app [main] {
    pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br"
}

import pf.Stdout
import pf.Stdin
import pf.Task exposing [Task]

main : Task {} []
main =
    Stdout.line! "Hello, World!"
    Stdout.line! "Enter your name:"
    name = Stdin.line!
    Stdout.line! "Hello, \(name)!"
```

**Key shift:** Elm's declarative Model-Update-View loop becomes Roc's imperative Task chain.

**Note:** The Roc version is CLI-based because Roc doesn't target the browser. For equivalent browser functionality, you'd need a Roc web platform (still in development).

---

## Type System Mapping

### Primitive Types

| Elm | Roc | Notes |
|-----|-----|-------|
| `True` / `False` | `Bool.true` / `Bool.false` | Capitalization differs |
| `42` | `42` | Integer literals |
| `3.14` | `3.14` | Float literals |
| `"text"` | `"text"` | String literals |
| `Int` | `I64` or `U64` | Elm has arbitrary precision, Roc has sized types |
| `Float` | `F64` or `F32` | Elm has single Float, Roc has sized types |
| `String` | `Str` | Direct mapping |
| `Char` | `U32` | Roc treats chars as Unicode scalar values |

### Collection Types

| Elm | Roc | Notes |
|-----|-----|-------|
| `List a` | `List a` | Identical syntax and semantics |
| `Dict comparable v` | `Dict k v` | Roc requires `k` to implement Hash & Eq |
| `Set comparable` | `Set a` | Roc requires `a` to implement Hash & Eq |
| `( a, b )` | `(a, b)` | Tuples (Roc supports arbitrary tuple sizes) |
| `Array a` | `List a` | Elm's Array → Roc's List (Roc optimizes internally) |

### Record Types

| Elm | Roc | Notes |
|-----|-----|-------|
| `{ name : String, age : Int }` | `{ name : Str, age : U32 }` | Nearly identical, just type name differences |
| `{ user \| age = 31 }` | `{ user & age: 31 }` | Record update syntax differs (\| vs &, = vs :) |
| `{ name, age } = user` | `{ name, age } = user` | Destructuring identical |
| `user.name` | `user.name` | Field access identical |

### Custom Types to Tag Unions

**Elm:**
```elm
-- Named custom type (nominal)
type Color
    = Red
    | Green
    | Blue
    | Custom Int Int Int

type alias RGB =
    { r : Int, g : Int, b : Int }
```

**Roc:**
```roc
# Structural tag union
Color : [Red, Green, Blue, Custom(U8, U8, U8)]

# Record type alias
RGB : { r : U8, g : U8, b : U8 }
```

**Key differences:**
- Elm requires explicit `type` declaration
- Roc uses structural types (no declaration needed)
- Elm uses type constructors with `|`
- Roc uses tag union syntax with `[]`

### Optional Values

**Elm:**
```elm
-- Built-in Maybe type
email : Maybe String
email = Just "alice@example.com"

-- Pattern match
emailText : String
emailText =
    case email of
        Just addr ->
            addr
        Nothing ->
            "no email"

-- Helper functions
emailOrDefault : String
emailOrDefault =
    Maybe.withDefault "no email" email
```

**Roc:**
```roc
# Inline tag union (no built-in Maybe)
email : [Some Str, None]
email = Some("alice@example.com")

# Pattern match
emailText : Str
emailText =
    when email is
        Some(addr) -> addr
        None -> "no email"

# Manual helper or use Result
```

**Translation:**
- `Maybe a` → `[Some a, None]`
- `Just value` → `Some(value)`
- `Nothing` → `None`

### Result Type (Parameter Order Reversed!)

**Elm:**
```elm
-- Result error ok
divide : Int -> Int -> Result String Int
divide a b =
    if b == 0 then
        Err "Division by zero"
    else
        Ok (a // b)
```

**Roc:**
```roc
# Result ok err (REVERSED!)
divide : I64, I64 -> Result I64 Str
divide = \a, b ->
    if b == 0 then
        Err("Division by zero")
    else
        Ok(a // b)
```

**CRITICAL:**
- Elm's `Result error ok` has error first
- Roc's `Result ok err` has success first
- **Always reverse the parameter order** when converting

---

## Idiom Translation

### 1. Pattern Matching: case → when

**Elm:**
```elm
classify : Int -> String
classify n =
    case n of
        0 ->
            "zero"

        x ->
            if x < 0 then
                "negative"
            else
                "positive"
```

**Roc:**
```roc
classify : I64 -> Str
classify = \n ->
    when n is
        0 -> "zero"
        x if x < 0 -> "negative"
        _ -> "positive"
```

**Why this translation:**
- Elm's `case` becomes Roc's `when`
- Elm uses `if` expressions in branches, Roc has guard clauses (`if` after pattern)
- Roc allows inline guards which are more concise

### 2. List Processing

**Elm:**
```elm
doubled : List Int
doubled =
    List.map (\x -> x * 2) [ 1, 2, 3, 4, 5 ]

-- Pipeline style
result : Int
result =
    [ 1, 2, 3, 4, 5 ]
        |> List.map (\x -> x * 2)
        |> List.filter (\x -> x > 5)
        |> List.foldl (+) 0
```

**Roc:**
```roc
doubled : List I64
doubled = List.map([1, 2, 3, 4, 5], \x -> x * 2)

# Pipeline style (same!)
result : I64
result = [1, 2, 3, 4, 5]
    |> List.map(\x -> x * 2)
    |> List.keepIf(\x -> x > 5)
    |> List.walk(0, Num.add)
```

**Why this translation:**
- `List.filter` → `List.keepIf` (different name, same semantics)
- `List.foldl` / `List.foldr` → `List.walk` (different name)
- Pipeline operator `|>` is identical
- Roc supports both `List.map(list, fn)` and `List.map list fn` syntax

### 3. Record Updates

**Elm:**
```elm
user =
    { name = "Alice", age = 30 }

olderUser =
    { user | age = 31 }

-- Multiple fields
updatedUser =
    { user
        | age = 31
        , name = "Alice Smith"
    }
```

**Roc:**
```roc
user = { name: "Alice", age: 30 }
olderUser = { user & age: 31 }

# Multiple fields
updatedUser = { user &
    age: 31,
    name: "Alice Smith"
}
```

**Why this translation:**
- Elm uses `|` for updates, Roc uses `&`
- Elm uses `=` for field assignment, Roc uses `:`
- Syntax is almost identical otherwise

### 4. Cmd/Task → Task

**Elm:**
```elm
type Msg
    = GotData (Result Http.Error String)

fetchData : Cmd Msg
fetchData =
    Http.get
        { url = "https://api.example.com/data"
        , expect = Http.expectString GotData
        }

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FetchData ->
            ( model, fetchData )

        GotData result ->
            case result of
                Ok data ->
                    ( { model | data = String.toUpper data }
                    , Cmd.none
                    )

                Err _ ->
                    ( { model | error = Just "Failed" }
                    , Cmd.none
                    )
```

**Roc:**
```roc
main : Task {} []
main =
    data = Http.get!("https://api.example.com/data")
    processed = Str.toUpper(data)
    Task.ok({})
```

**Why this translation:**
- Elm's Cmd with Msg handling becomes Roc's direct Task chaining
- Elm's `Task.perform` becomes Roc's `!` operator
- Elm's event-driven model becomes Roc's sequential execution
- No Model or Msg types needed in Roc for simple cases

### 5. Error Propagation

**Elm:**
```elm
calculate : Int -> Int -> Int -> Result String Int
calculate a b c =
    divide a b
        |> Result.andThen (\x -> divide x c)

-- Or with explicit pattern matching
calculateExplicit : Int -> Int -> Int -> Result String Int
calculateExplicit a b c =
    case divide a b of
        Err e ->
            Err e
        Ok x ->
            case divide x c of
                Err e ->
                    Err e
                Ok y ->
                    Ok y
```

**Roc:**
```roc
# Roc: Result ok err (reversed params!)
calculate : I64, I64, I64 -> Result I64 Str
calculate = \a, b, c ->
    x = divide!(a, b)  # Returns early on Err
    y = divide!(x, c)  # Returns early on Err
    Ok(y)
```

**Why this translation:**
- Elm's `Result.andThen` becomes Roc's `!` operator
- Roc's `!` provides automatic early return on error
- Much more concise than Elm's explicit chaining
- Remember to **reverse Result type parameters**!

### 6. Opaque Types

**Elm:**
```elm
-- Elm uses module visibility for opacity
module Age exposing (Age, create, toInt)

type Age
    = Age Int

create : Int -> Maybe Age
create n =
    if n >= 0 && n < 150 then
        Just (Age n)
    else
        Nothing

toInt : Age -> Int
toInt (Age n) =
    n

-- Constructor Age is NOT exposed, only create function
```

**Roc:**
```roc
interface Age
    exposes [Age, create, toU32]
    imports []

# Opaque type
Age := U32

create : U32 -> Result Age [InvalidAge]
create = \n ->
    if n >= 0 && n < 150 then
        Ok(@Age(n))
    else
        Err(InvalidAge)

toU32 : Age -> U32
toU32 = \@Age(n) -> n
```

**Why this translation:**
- Elm uses pattern matching for unwrapping, Roc uses `@` syntax
- Both achieve opacity through module exports
- Roc's `@Age(n)` wrapping is more explicit than Elm's `Age n`
- Roc uses `Result` for validation, Elm uses `Maybe` (different conventions)

---

## Paradigm Translation: TEA → Platform Model

### Mental Model Shift

| Elm Concept | Roc Approach | Key Insight |
|-------------|--------------|-------------|
| Model-Update-View loop | Task chain (sequential) | Declarative → Imperative |
| Browser provides events | Platform provides I/O | Browser → CLI/Native |
| `main` returns Program | `main` returns Task | Pure → Effect |
| Cmd issued, Msg received | Tasks compose with `!` | Indirect → Direct |

### Effect System Comparison

| Elm Model | Roc Model | Conceptual Translation |
|-----------|-----------|------------------------|
| Runtime handles Cmd/Sub | Platform handles Tasks | Both managed by runtime |
| Asynchronous with Msg | Sequential with `!` | Event-driven → Chain |
| Cmd.none / new Cmd | Task.ok/Task.err | Side effect → Return value |

### Example: HTTP Fetch

**Elm (TEA):**
```elm
type alias Model =
    { users : RemoteData Http.Error (List User)
    }

type Msg
    = FetchUsers
    | GotUsers (Result Http.Error (List User))

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FetchUsers ->
            ( { model | users = Loading }
            , Http.get
                { url = "/api/users"
                , expect = Http.expectJson GotUsers usersDecoder
                }
            )

        GotUsers result ->
            case result of
                Ok users ->
                    ( { model | users = Success users }, Cmd.none )

                Err error ->
                    ( { model | users = Failure error }, Cmd.none )
```

**Roc (Platform):**
```roc
main : Task {} []
main =
    users = Http.get!("/api/users")
    decoded = Decode.fromBytes!(users, usersDecoder)
    Stdout.line!("Fetched \(List.len(decoded) |> Num.toStr) users")
```

**Key differences:**
- Elm models loading states explicitly
- Roc handles success/error sequentially
- Elm's async becomes Roc's sequential (platform handles concurrency)
- No Model or Msg types needed in Roc

---

## Error Handling

### Elm Result → Roc Result

**Key Difference: Parameter order is reversed!**

```elm
-- Elm: Result err ok
parseAge : String -> Result String Int
parseAge str =
    case String.toInt str of
        Just age ->
            if age >= 0 then
                Ok age
            else
                Err "Negative age"

        Nothing ->
            Err "Not a number"
```

```roc
# Roc: Result ok err (REVERSED!)
parseAge : Str -> Result U32 Str
parseAge = \str ->
    when Str.toU32(str) is
        Ok(age) if age >= 0 -> Ok(age)
        Ok(_) -> Err("Negative age")
        Err(_) -> Err("Not a number")
```

### Error Type Modeling

**Elm uses named custom types:**
```elm
type FetchError
    = NetworkError Http.Error
    | NotFound
    | Unauthorized

fetchUser : Int -> Task FetchError User
```

**Roc uses inline tag unions:**
```roc
fetchUser : U64 -> Task User [NetworkError, NotFound, Unauthorized]
```

**Translation:**
- Elm's named error types → Roc's inline tag unions
- Same expressiveness, less ceremony
- Roc is structural, Elm is nominal

---

## Effect System Translation

### Cmd in Elm vs Task in Roc

**Elm Cmd (event-driven):**
```elm
type Msg
    = GotData (Result Http.Error String)

fetchData : Cmd Msg
fetchData =
    Http.get
        { url = "https://api.example.com/data"
        , expect = Http.expectString GotData
        }

-- Must handle result in update function
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GotData result ->
            -- Handle result here
            ...
```

**Roc Task (sequential):**
```roc
fetchData : Task Str []
fetchData =
    Http.get!("https://api.example.com/data")
```

**Why this translation:**
- Elm's Cmd is fire-and-forget, result comes via Msg
- Roc's Task chains sequentially with `!`
- Elm separates effect from handling, Roc combines them

### Sub in Elm vs Task in Roc

**Elm Subscriptions:**
```elm
subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ Time.every 1000 Tick
        , Browser.Events.onResize WindowResized
        ]
```

**Roc approach:**
Roc doesn't have built-in subscriptions. Platforms may provide equivalent mechanisms through Task-based polling or event streams, but this is platform-specific.

For periodic tasks, you'd typically use platform-specific APIs or structure your `main` Task to loop.

---

## Module System Translation

### Elm Modules → Roc Interfaces

**Elm:**
```elm
module User exposing (User, create, getName, getAge)

type alias User =
    { name : String
    , age : Int
    }

create : String -> Int -> User
create name age =
    { name = name, age = age }

getName : User -> String
getName user =
    user.name

getAge : User -> Int
getAge user =
    user.age
```

**Roc:**
```roc
interface User
    exposes [User, create, getName, getAge]
    imports []

User : {
    name : Str,
    age : U32,
}

create : Str, U32 -> User
create = \name, age ->
    { name, age }

getName : User -> Str
getName = \user -> user.name

getAge : User -> U32
getAge = \user -> user.age
```

**Translation:**
- `module` → `interface`
- `exposing` → `exposes`
- `type alias` → type annotation
- Same visibility model (only exposed items are public)

### Import Patterns

**Elm:**
```elm
import Dict
import Dict exposing (Dict)
import List exposing (map, filter)
import Maybe exposing (Maybe(..))
import Html as H
import Html.Events as Events
```

**Roc:**
```roc
import Dict
import Dict exposing [Dict]
import List exposing [map, keepIf]
import pf.Stdout
import pf.Task exposing [Task]

# Note: Roc doesn't have import aliasing yet
# Must use full qualified names
```

**Translation:**
- `exposing` → `exposing`
- Parentheses `()` → Brackets `[]`
- Elm's `import as` → Not yet available in Roc

---

## Common Pitfalls

1. **Result parameter order reversal (MOST CRITICAL)**
   - Elm: `Result err ok`
   - Roc: `Result ok err`
   - **Always reverse parameters** when converting Result types
   - Double-check every Result type signature!

2. **Record update syntax**
   - Elm: `{ record | field = value }`
   - Roc: `{ record & field: value }`
   - Don't mix up `|`/`&` and `=`/`:`

3. **Case vs When syntax**
   - Elm: `case x of`
   - Roc: `when x is`
   - Remember `is` not `of`

4. **Platform target mismatch**
   - Elm targets browser only (DOM, HTML, CSS)
   - Roc is platform-agnostic (CLI, native, potentially web)
   - Browser-specific Elm code needs redesign

5. **Bang operator vs explicit Task**
   - Elm: No `!` operator, use `Task.perform` or `Cmd`
   - Roc: `value = task!` for sequential execution
   - Much more concise in Roc

6. **Capitalization**
   - Elm: `True`, `False`
   - Roc: `Bool.true`, `Bool.false`
   - Watch for True/False differences

7. **Function types**
   - Elm: `a -> b -> c` (curried)
   - Roc: `a, b -> c` (comma-separated params)
   - Roc allows both, but commas are clearer

8. **Maybe vs tag union**
   - Elm: Built-in `Maybe a` with `Just`/`Nothing`
   - Roc: Use `[Some a, None]` (no built-in Maybe)
   - Must define tag union explicitly

9. **List function names**
   - Elm: `List.filter`, `List.foldl`, `List.foldr`
   - Roc: `List.keepIf`, `List.walk`
   - Same concepts, different names

10. **String interpolation**
    - Elm: `"Hello, " ++ name ++ "!"`
    - Roc: `"Hello, \(name)!"`
    - Roc has built-in string interpolation

---

## Tooling

| Tool | Elm | Roc | Notes |
|------|-----|-----|-------|
| Formatter | `elm-format` | `roc format` | Both enforce standard style |
| REPL | `elm repl` | `roc repl` | Both support interactive testing |
| Test | `elm-test` | `roc test` | Different syntax (case vs expect) |
| Build | `elm make` | `roc build` | Elm → JavaScript, Roc → native |
| Package manager | `elm install` | Platform URLs | Roc uses URL-based dependencies |
| Linter | `elm-review` | N/A | Elm has rich linting, Roc doesn't yet |

---

## Examples

### Example 1: Simple - Type and Function Translation

**Before (Elm):**
```elm
type alias User =
    { name : String
    , age : Int
    }

greet : User -> String
greet user =
    "Hello, " ++ user.name ++ "! You are " ++ String.fromInt user.age ++ " years old."

-- Test
import Test exposing (test)
import Expect

suite =
    test "greet formats message correctly" <|
        \_ ->
            greet { name = "Alice", age = 30 }
                |> Expect.equal "Hello, Alice! You are 30 years old."
```

**After (Roc):**
```roc
User : { name : Str, age : U32 }

greet : User -> Str
greet = \user ->
    "Hello, \(user.name)! You are \(Num.toStr(user.age)) years old."

expect greet({ name: "Alice", age: 30 }) == "Hello, Alice! You are 30 years old."
```

**Key changes:**
- `String` → `Str`, `Int` → `U32`
- String concatenation `++` → interpolation `\(...)`
- `String.fromInt` → `Num.toStr`
- Elm's separate test file → inline `expect`
- `type alias` → type annotation

### Example 2: Medium - Custom Types and Pattern Matching

**Before (Elm):**
```elm
type Color
    = Red
    | Green
    | Blue
    | Custom Int Int Int

toHex : Color -> String
toHex color =
    case color of
        Red ->
            "#FF0000"

        Green ->
            "#00FF00"

        Blue ->
            "#0000FF"

        Custom r g b ->
            "#" ++ toHexByte r ++ toHexByte g ++ toHexByte b

toHexByte : Int -> String
toHexByte n =
    -- Implementation using Hex library
    String.fromInt n  -- Simplified
```

**After (Roc):**
```roc
Color : [Red, Green, Blue, Custom(U8, U8, U8)]

toHex : Color -> Str
toHex = \color ->
    when color is
        Red -> "#FF0000"
        Green -> "#00FF00"
        Blue -> "#0000FF"
        Custom(r, g, b) ->
            "#\(toHexByte(r))\(toHexByte(g))\(toHexByte(b))"

toHexByte : U8 -> Str
toHexByte = \n ->
    # Implementation
    Num.toStr(n)  # Simplified
```

**Key changes:**
- Named `type` declaration → Structural tag union
- `case x of` → `when x is`
- `Int` → `U8` (Roc has sized integers)
- String concatenation → interpolation
- Constructor `Custom r g b` → `Custom(r, g, b)`

### Example 3: Complex - TEA to Platform Model

**Before (Elm):**
```elm
module Main exposing (main)

import Browser
import Html exposing (Html, div, text, button)
import Html.Events exposing (onClick)
import Http
import Json.Decode as Decode exposing (Decoder)

-- MODEL

type alias User =
    { id : Int
    , name : String
    , email : String
    }

type RemoteData e a
    = NotAsked
    | Loading
    | Success a
    | Failure e

type alias Model =
    { user : RemoteData Http.Error User
    }

init : () -> ( Model, Cmd Msg )
init _ =
    ( { user = NotAsked }, Cmd.none )

-- UPDATE

type Msg
    = FetchUser
    | GotUser (Result Http.Error User)

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FetchUser ->
            ( { model | user = Loading }
            , fetchUser 1
            )

        GotUser result ->
            case result of
                Ok user ->
                    ( { model | user = Success user }
                    , Cmd.none
                    )

                Err error ->
                    ( { model | user = Failure error }
                    , Cmd.none
                    )

-- HTTP

fetchUser : Int -> Cmd Msg
fetchUser userId =
    Http.get
        { url = "https://api.example.com/users/" ++ String.fromInt userId
        , expect = Http.expectJson GotUser userDecoder
        }

userDecoder : Decoder User
userDecoder =
    Decode.map3 User
        (Decode.field "id" Decode.int)
        (Decode.field "name" Decode.string)
        (Decode.field "email" Decode.string)

-- VIEW

view : Model -> Html Msg
view model =
    div []
        [ case model.user of
            NotAsked ->
                button [ onClick FetchUser ] [ text "Fetch User" ]

            Loading ->
                text "Loading..."

            Success user ->
                div []
                    [ text ("User: " ++ user.name ++ " (" ++ user.email ++ ")")
                    ]

            Failure error ->
                text ("Error: " ++ httpErrorToString error)
        ]

httpErrorToString : Http.Error -> String
httpErrorToString error =
    case error of
        Http.BadUrl url ->
            "Bad URL: " ++ url

        Http.Timeout ->
            "Timeout"

        Http.NetworkError ->
            "Network error"

        Http.BadStatus status ->
            "Bad status: " ++ String.fromInt status

        Http.BadBody body ->
            "Bad body: " ++ body

-- MAIN

main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , view = view
        , subscriptions = \_ -> Sub.none
        }
```

**After (Roc):**
```roc
app [main] {
    pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br"
}

import pf.Http
import pf.Stdout
import pf.Task exposing [Task]
import json.Decode

# Note: Result type parameters REVERSED!
# Elm: Result Http.Error User
# Roc: Result User [HttpErr]

User : { id : U64, name : Str, email : Str }

fetchUser : U64 -> Task User [HttpErr, DecodeErr]
fetchUser = \userId ->
    url = "https://api.example.com/users/\(Num.toStr(userId))"
    response = Http.get!(url)

    when Decode.fromBytes(response.body, userDecoder) is
        Ok(user) -> Task.ok(user)
        Err(err) -> Task.err(DecodeErr)

userDecoder : Decode.Decoder User
userDecoder =
    Decode.record(\field ->
        {
            id: field.required("id", Decode.u64),
            name: field.required("name", Decode.str),
            email: field.required("email", Decode.str),
        }
    )

main : Task {} []
main =
    when fetchUser(1) is
        Ok(user) ->
            Stdout.line!("User: \(user.name) (\(user.email))")

        Err(HttpErr) ->
            Stdout.line!("HTTP error occurred")

        Err(DecodeErr) ->
            Stdout.line!("Failed to decode user")
```

**Key changes:**
- Elm's TEA (Model-Update-View) → Roc's Task chain
- Elm's `Cmd Msg` handling → Roc's `!` operator
- Elm's HTML view → Roc's CLI output
- Elm's loading states → Roc's direct execution
- Elm's `Result Http.Error User` → Roc's `Result User [HttpErr, DecodeErr]` (**reversed params!**)
- No Model, Msg, or update function needed
- Direct error handling with pattern matching

---

## Testing Translation

### Elm's elm-test → Roc's expect

**Elm:**
```elm
-- tests/UserTests.elm
module UserTests exposing (suite)

import Test exposing (Test, describe, test)
import Expect
import User

suite : Test
suite =
    describe "User module"
        [ describe "greet"
            [ test "formats greeting correctly" <|
                \_ ->
                    User.greet { name = "Alice", age = 30 }
                        |> Expect.equal "Hello, Alice! You are 30 years old."

            , test "handles young age" <|
                \_ ->
                    User.greet { name = "Bob", age = 5 }
                        |> Expect.equal "Hello, Bob! You are 5 years old."
            ]
        ]
```

**Roc:**
```roc
# User.roc
interface User
    exposes [User, greet]
    imports []

User : { name : Str, age : U32 }

greet : User -> Str
greet = \user ->
    "Hello, \(user.name)! You are \(Num.toStr(user.age)) years old."

# Inline tests
expect greet({ name: "Alice", age: 30 }) == "Hello, Alice! You are 30 years old."
expect greet({ name: "Bob", age: 5 }) == "Hello, Bob! You are 5 years old."
```

**Translation:**
- Elm's separate test files → Roc's inline `expect`
- Elm's `describe` and `test` → Roc's flat `expect` statements
- Elm's `Expect.equal` → Roc's `==` operator
- Run with `elm-test` → Run with `roc test`

---

## See Also

For more examples and patterns, see:
- `meta-convert-dev` - Foundational patterns with cross-language examples
- `convert-roc-elm` - Reverse conversion (Roc → Elm)
- `convert-elm-haskell` - Similar functional language conversion patterns
- `lang-elm-dev` - Elm development patterns
- `lang-roc-dev` - Roc development patterns

Cross-cutting pattern skills:
- `patterns-concurrency-dev` - Cmd/Sub vs Task comparison
- `patterns-serialization-dev` - JSON encoding/decoding across languages
- `patterns-metaprogramming-dev` - Why both languages avoid metaprogramming

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…