Claude-skill-registry convert-roc-elm

Convert Roc code to idiomatic Elm. Use when migrating Roc applications to Elm frontend code, translating platform-agnostic Roc to browser-based Elm, or refactoring Roc CLI tools to Elm web applications. Extends meta-convert-dev with Roc-to-Elm 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-roc-elm" ~/.claude/skills/majiayu000-claude-skill-registry-convert-roc-elm && rm -rf "$T"
manifest: skills/data/convert-roc-elm/SKILL.md
source content

Convert Roc to Elm

Convert Roc code to idiomatic Elm. This skill extends

meta-convert-dev
with Roc-to-Elm specific type mappings, idiom translations, and architectural patterns for moving from platform-agnostic Roc to browser-based Elm applications.

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: Roc types → Elm types
  • Idiom translations: Roc patterns → idiomatic Elm
  • Architecture patterns: Platform model → The Elm Architecture (TEA)
  • Effect system: Task → Cmd/Sub
  • Error handling: Result types (similar but different conventions)
  • Platform shift: General-purpose → Frontend-specific

This Skill Does NOT Cover

  • General conversion methodology - see
    meta-convert-dev
  • Roc language fundamentals - see
    lang-roc-dev
  • Elm language fundamentals - see
    lang-elm-dev
  • Reverse conversion (Elm → Roc) - see
    convert-elm-roc
  • Backend-specific Roc code - Elm is frontend-only

Quick Reference

RocElmNotes
Str
String
Direct mapping
I64
,
U64
Int
Elm has arbitrary precision integers
F64
Float
Direct mapping
Bool
Bool
Direct mapping with capitalization
List a
List a
Same syntax and operations
{ field : Type }
{ field : Type }
Records are nearly identical
[Tag1, Tag2]
type Custom = Tag1 | Tag2
Tag unions → Custom types
Result a e
Result e a
Reversed parameter order!
[Some a, None]
Maybe a
Optional values
Task a err
Cmd Msg
or
Task Never a
Effect systems differ
when x is
case x of
Pattern matching syntax
!
suffix operator
Task.perform
Bang operator → explicit Task handling

Architectural Paradigm Shift

From Platform Model to The Elm Architecture

AspectRoc Platform ModelElm TEA
TargetAny platform (CLI, web, native)Browser frontend only
EffectsPlatform-provided TaskRuntime-managed Cmd/Sub
Entry point
main : Task {} []
main : Program () Model Msg
StateImplicit in Task chainExplicit Model
UpdatesTask compositionupdate : Msg → Model → (Model, Cmd Msg)
I/OPlatform exposes (File, Http, etc.)Browser.* modules only

Roc Platform Application

app [main] {
    pf: platform "https://github.com/roc-lang/basic-cli/..."
}

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

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

Elm Equivalent Using TEA

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
        }

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


Type System Mapping

Primitive Types

RocElmNotes
Bool.true
/
Bool.false
True
/
False
Capitalization differs
42
42
Integer literals (Elm has arbitrary precision)
3.14
3.14
Float literals
"text"
"text"
String literals (Roc uses Str, Elm uses String)
I8, I16, I32, I64, I128
Int
Elm has single Int type (arbitrary precision)
U8, U16, U32, U64, U128
Int
Same - map to Int
F32, F64
Float
Elm has single Float type
Num a
number
Flexible number type (inferred)

Collection Types

RocElmNotes
List a
List a
Identical syntax and semantics
Dict k v
Dict k v
Same interface, import from Dict module
Set a
Set a
Same interface, import from Set module
(a, b)
( a, b )
Tuples (Elm supports up to 3-tuples idiomatically)

Record Types

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

Tag Unions to Custom Types

Roc:

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

handleColor : Color -> Str
handleColor = \color ->
    when color is
        Red -> "red"
        Green -> "green"
        Blue -> "blue"
        Custom(r, g, b) -> "rgb(\(Num.toStr(r)), ...)"

Elm:

-- Named custom type (nominal)
type Color
    = Red
    | Green
    | Blue
    | Custom Int Int Int

handleColor : Color -> String
handleColor color =
    case color of
        Red ->
            "red"
        Green ->
            "green"
        Blue ->
            "blue"
        Custom r g b ->
            "rgb(" ++ String.fromInt r ++ ", ...)"

Key differences:

  • Roc uses structural types (no declaration needed)
  • Elm requires explicit
    type
    declaration
  • Roc uses lowercase for type variables in tag payloads
  • Elm uses type constructors with capital letters

Optional Values

Roc:

# Inline tag union
email : [Some Str, None]
email = Some("alice@example.com")

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

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

Translation:

  • [Some a, None]
    Maybe a
  • Some(value)
    Just value
  • None
    Nothing

Result Type (Parameter Order Reversed!)

Roc:

# Result ok err
divide : I64, I64 -> Result I64 [DivByZero]
divide = \a, b ->
    if b == 0 then
        Err(DivByZero)
    else
        Ok(a // b)

Elm:

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

CRITICAL: Roc's

Result ok err
becomes Elm's
Result error ok
- parameters are reversed!


Idiom Translation

1. Pattern Matching: when → case

Roc:

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

Elm:

classify : Int -> String
classify n =
    case n of
        0 ->
            "zero"
        x ->
            if x < 0 then
                "negative"
            else
                "positive"

Why this translation:

  • Roc's
    when
    becomes Elm's
    case
  • Roc has guard clauses (
    if
    after pattern), Elm uses
    if
    expressions in branches
  • Elm requires explicit
    ->
    and indentation

2. List Processing

Roc:

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

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

Elm:

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

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

Why this translation:

  • List.keepIf
    List.filter
    (different name)
  • List.walk
    List.foldl
    or
    List.foldr
    (different name)
  • Pipeline operator
    |>
    is identical
  • Elm uses function-first, args-last (currying)

3. Record Updates

Roc:

user = { name: "Alice", age: 30 }
olderUser = { user & age: 31 }

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

Elm:

user =
    { name = "Alice", age = 30 }

olderUser =
    { user | age = 31 }

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

Why this translation:

  • Roc uses
    &
    for updates, Elm uses
    |
  • Roc uses
    :
    for field assignment, Elm uses
    =
  • Syntax is almost identical otherwise

4. Task-Based Effects → Cmd/Task

Roc:

main : Task {} []
main =
    content = File.readUtf8!("input.txt")
    processed = String.toUpper(content)
    File.writeUtf8!("output.txt", processed)
    Stdout.line!("Done!")

Elm:

-- Elm doesn't have file access (browser only)
-- This example shows HTTP instead

type Msg
    = GotData (Result Http.Error String)
    | DataProcessed

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FetchData ->
            ( { model | loading = True }
            , Http.get
                { url = "/api/data"
                , expect = Http.expectString GotData
                }
            )

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

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

Why this translation:

  • Roc's
    !
    suffix operator becomes explicit Cmd in Elm
  • Roc chains Tasks sequentially; Elm uses Model updates
  • File I/O doesn't exist in Elm (browser sandbox)
  • Must model async operations as Msg and handle in update

5. Error Propagation

Roc:

calculate : I64, I64, I64 -> Result I64 [DivByZero]
calculate = \a, b, c ->
    x = divide!(a, b)  # Returns early on Err
    y = divide!(x, c)  # Returns early on Err
    Ok(y)

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

Why this translation:

  • Roc's
    !
    operator doesn't exist in Elm
  • Use
    Result.andThen
    for chaining (monadic bind)
  • Or use explicit
    case
    expressions

6. Opaque Types

Roc:

Age := U32

createAge : U32 -> Age
createAge = \n -> @Age(n)

getAge : Age -> U32
getAge = \@Age(n) -> n

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

Why this translation:

  • Roc uses
    @
    unwrapping syntax, Elm uses pattern matching
  • Elm achieves opacity through module exports (
    Age
    type exposed, constructor hidden)
  • Elm typically adds validation in smart constructors

Paradigm Translation: Platform Model → TEA

Mental Model Shift

Roc ConceptElm ApproachKey Insight
Task chain (sequential)Model-Update-View loopImperative → Declarative
Platform provides I/OBrowser provides eventsCLI/Native → Browser
main
returns Task
main
returns Program
Effect → Pure
Tasks compose with
!
Cmd issued, Msg receivedDirect → Indirect

Concurrency Mental Model

Roc ModelElm ModelConceptual Translation
Platform handles TasksRuntime handles Cmd/SubBoth managed by runtime
Sequential with
!
Asynchronous with MsgChain → Event-driven
Task.ok/Task.errCmd.none / new CmdReturn value → Side effect

Error Handling

Roc Result → Elm Result

Key Difference: Parameter order is reversed!

-- Roc: Result ok err
parseAge : Str -> Result U32 [ParseError Str, NegativeAge]
parseAge = \str ->
    when Str.toU32(str) is
        Ok(age) if age >= 0 -> Ok(age)
        Ok(_) -> Err(NegativeAge)
        Err(_) -> Err(ParseError("Not a number"))
-- Elm: Result err ok (REVERSED!)
type ParseError
    = NotANumber
    | NegativeAge

parseAge : String -> Result ParseError Int
parseAge str =
    case String.toInt str of
        Just age ->
            if age >= 0 then
                Ok age
            else
                Err NegativeAge

        Nothing ->
            Err NotANumber

Error Type Modeling

Roc uses inline tag unions:

fetchUser : U64 -> Task User [NetworkError, NotFound, Unauthorized]

Elm uses named custom types:

type FetchError
    = NetworkError Http.Error
    | NotFound
    | Unauthorized

fetchUser : Int -> Task FetchError User

Effect System Translation

Task in Roc vs Task/Cmd in Elm

Roc Task:

# Platform-provided, sequential execution
fetchAndProcess : Task Str []
fetchAndProcess =
    data = Http.get!("https://api.example.com/data")
    processed = String.toUpper(data)
    Task.ok(processed)

Elm Cmd (most common):

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
                    )

Elm Task (advanced):

import Task

-- For chaining async operations
fetchAndProcess : Task Http.Error String
fetchAndProcess =
    Http.task
        { method = "GET"
        , headers = []
        , url = "https://api.example.com/data"
        , body = Http.emptyBody
        , resolver = Http.stringResolver handleResponse
        , timeout = Nothing
        }
        |> Task.map String.toUpper

-- Convert to Cmd
performFetch : Cmd Msg
performFetch =
    Task.attempt GotData fetchAndProcess

Common Pitfalls

  1. Result parameter order reversal

    • Roc:
      Result ok err
    • Elm:
      Result err ok
    • Always swap parameters when converting Result types
  2. Record update syntax

    • Roc:
      { record & field: value }
    • Elm:
      { record | field = value }
    • Don't mix up
      &
      /
      |
      and
      :
      /
      =
  3. Case vs When syntax

    • Roc:
      when x is
    • Elm:
      case x of
    • Remember
      of
      not
      is
  4. Module target mismatch

    • Roc CLI/native modules (File, Stdout) don't exist in Elm
    • Must redesign as browser-based UI
    • No file I/O, only HTTP and localStorage
  5. Bang operator translation

    • Roc:
      value = task!
    • Elm: Must use
      Task.andThen
      or Cmd with Msg handling
    • No direct equivalent to
      !
      operator
  6. Capitalization

    • Roc:
      Bool.true
    • Elm:
      True
    • Watch for True/False vs true/false
  7. Function application

    • Both use space for application, but Elm heavily uses currying
    • Roc:
      List.map(list, fn)
      or
      List.map list fn
    • Elm:
      List.map fn list
      (function first)

Tooling

ToolRocElmNotes
Formatter
roc format
elm-format
Both enforce standard style
REPL
roc repl
elm repl
Both support interactive testing
Test
roc test
elm-test
Different syntax, same concept
Build
roc build
elm make
Roc → native, Elm → JavaScript
Package managerPlatform URLs
elm install
Elm has centralized package repo
LinterN/A
elm-review
Elm has rich linting ecosystem

Examples

Example 1: Simple - Type and Function Translation

Before (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."

After (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 (in tests/ directory)
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."

Key changes:

  • Str
    String
    ,
    U32
    Int
  • String interpolation
    \(...)
    → concatenation
    ++
  • Num.toStr
    String.fromInt
  • expect
    → separate test file with
    Test
    module

Example 2: Medium - Tag Unions and Pattern Matching

Before (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 details...

After (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 or String.fromInt with base conversion
    String.fromInt n  -- Simplified for example

Key changes:

  • Structural tag union → Named
    type
    declaration
  • when x is
    case x of
  • U8
    Int
    (Elm doesn't have sized integers)
  • String interpolation → concatenation

Example 3: Complex - Task Chain to TEA

Before (Roc):

app [main] {
    pf: platform "https://github.com/roc-lang/basic-cli/..."
}

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

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

fetchUser : U64 -> Task User [HttpError]
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(_) -> Task.err(HttpError)

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

After (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
        }

Key changes:

  • Roc's imperative Task chain → Elm's TEA (Model-Update-View)
  • Roc's
    !
    operator → Elm's Cmd and Msg handling
  • Roc's CLI output → Elm's HTML view
  • Roc's sequential execution → Elm's event-driven updates
  • Added loading states (NotAsked, Loading, Success, Failure)
  • Explicit error handling in view

See Also

For more examples and patterns, see:

  • meta-convert-dev
    - Foundational patterns with cross-language examples
  • convert-erlang-elm
    - Similar backend-to-frontend conversion patterns
  • lang-roc-dev
    - Roc development patterns
  • lang-elm-dev
    - Elm development patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev
    - Task vs Cmd/Sub comparison
  • patterns-serialization-dev
    - JSON encoding/decoding across languages