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.
git clone https://github.com/majiayu000/claude-skill-registry
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"
skills/data/convert-roc-elm/SKILL.mdConvert 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
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
This Skill Adds
- Type mappings: 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
| Roc | Elm | Notes |
|---|---|---|
| | Direct mapping |
, | | Elm has arbitrary precision integers |
| | Direct mapping |
| | Direct mapping with capitalization |
| | Same syntax and operations |
| | Records are nearly identical |
| | Tag unions → Custom types |
| | Reversed parameter order! |
| | Optional values |
| or | Effect systems differ |
| | Pattern matching syntax |
suffix operator | | Bang operator → explicit Task handling |
Architectural Paradigm Shift
From Platform Model to The Elm Architecture
| Aspect | Roc Platform Model | Elm TEA |
|---|---|---|
| Target | Any platform (CLI, web, native) | Browser frontend only |
| Effects | Platform-provided Task | Runtime-managed Cmd/Sub |
| Entry point | | |
| State | Implicit in Task chain | Explicit Model |
| Updates | Task composition | update : Msg → Model → (Model, Cmd Msg) |
| I/O | Platform 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
| Roc | Elm | Notes |
|---|---|---|
/ | / | Capitalization differs |
| | Integer literals (Elm has arbitrary precision) |
| | Float literals |
| | String literals (Roc uses Str, Elm uses String) |
| | Elm has single Int type (arbitrary precision) |
| | Same - map to Int |
| | Elm has single Float type |
| | Flexible number type (inferred) |
Collection Types
| Roc | Elm | Notes |
|---|---|---|
| | Identical syntax and semantics |
| | Same interface, import from Dict module |
| | Same interface, import from Set module |
| | Tuples (Elm supports up to 3-tuples idiomatically) |
Record Types
| Roc | Elm | Notes |
|---|---|---|
| | Nearly identical, just type name differences |
| | Record update syntax differs (& vs |) |
| | Destructuring identical |
| | 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
declarationtype - 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
→NoneNothing
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
becomes Elm'swhencase - Roc has guard clauses (
after pattern), Elm usesif
expressions in branchesif - 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
(different name)List.filter
→List.walk
orList.foldl
(different name)List.foldr- 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
for chaining (monadic bind)Result.andThen - Or use explicit
expressionscase
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 (
type exposed, constructor hidden)Age - Elm typically adds validation in smart constructors
Paradigm Translation: Platform Model → TEA
Mental Model Shift
| Roc Concept | Elm Approach | Key Insight |
|---|---|---|
| Task chain (sequential) | Model-Update-View loop | Imperative → Declarative |
| Platform provides I/O | Browser provides events | CLI/Native → Browser |
returns Task | returns Program | Effect → Pure |
Tasks compose with | Cmd issued, Msg received | Direct → Indirect |
Concurrency Mental Model
| Roc Model | Elm Model | Conceptual Translation |
|---|---|---|
| Platform handles Tasks | Runtime handles Cmd/Sub | Both managed by runtime |
Sequential with | Asynchronous with Msg | Chain → Event-driven |
| Task.ok/Task.err | Cmd.none / new Cmd | Return 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
-
Result parameter order reversal
- Roc:
Result ok err - Elm:
Result err ok - Always swap parameters when converting Result types
- Roc:
-
Record update syntax
- Roc:
{ record & field: value } - Elm:
{ record | field = value } - Don't mix up
/&
and|
/:=
- Roc:
-
Case vs When syntax
- Roc:
when x is - Elm:
case x of - Remember
notofis
- Roc:
-
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
-
Bang operator translation
- Roc:
value = task! - Elm: Must use
or Cmd with Msg handlingTask.andThen - No direct equivalent to
operator!
- Roc:
-
Capitalization
- Roc:
Bool.true - Elm:
True - Watch for True/False vs true/false
- Roc:
-
Function application
- Both use space for application, but Elm heavily uses currying
- Roc:
orList.map(list, fn)List.map list fn - Elm:
(function first)List.map fn list
Tooling
| Tool | Roc | Elm | Notes |
|---|---|---|---|
| Formatter | | | Both enforce standard style |
| REPL | | | Both support interactive testing |
| Test | | | Different syntax, same concept |
| Build | | | Roc → native, Elm → JavaScript |
| Package manager | Platform URLs | | Elm has centralized package repo |
| Linter | N/A | | 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
→U32Int- String interpolation
→ concatenation\(...)++
→Num.toStrString.fromInt
→ separate test file withexpect
moduleTest
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
declarationtype
→when x iscase x of
→U8
(Elm doesn't have sized integers)Int- 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:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Similar backend-to-frontend conversion patternsconvert-erlang-elm
- Roc development patternslang-roc-dev
- Elm development patternslang-elm-dev
Cross-cutting pattern skills:
- Task vs Cmd/Sub comparisonpatterns-concurrency-dev
- JSON encoding/decoding across languagespatterns-serialization-dev