Claude Code Plugins

Community-maintained marketplace

Feedback

convert-roc-haskell

@aRustyDev/ai
0
0

Convert Roc code to idiomatic Haskell. Use when migrating Roc projects to Haskell, translating Roc patterns to idiomatic Haskell, or refactoring Roc codebases. Extends meta-convert-dev with Roc-to-Haskell 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-roc-haskell
description Convert Roc code to idiomatic Haskell. Use when migrating Roc projects to Haskell, translating Roc patterns to idiomatic Haskell, or refactoring Roc codebases. Extends meta-convert-dev with Roc-to-Haskell specific patterns.

Convert Roc to Haskell

Convert Roc code to idiomatic Haskell. This skill extends meta-convert-dev with Roc-to-Haskell specific type mappings, idiom translations, and tooling for migrating platform-based Roc code to pure functional Haskell.

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 → Haskell types
  • Idiom translations: Roc patterns → idiomatic Haskell
  • Error handling: Roc Result → Haskell Either/Maybe
  • Platform model: Roc applications/platforms → Haskell IO/mtl
  • Abilities: Roc abilities → Haskell type classes
  • Tag unions: Roc structural tags → Haskell algebraic data types

This Skill Does NOT Cover

  • General conversion methodology - see meta-convert-dev
  • Roc language fundamentals - see lang-roc-dev
  • Haskell language fundamentals - see lang-haskell-dev
  • Reverse conversion (Haskell → Roc) - see convert-haskell-roc
  • Platform development - Both use different models; design from scratch

Quick Reference

Roc Haskell Notes
Str String or Text Use Text for production
U8, U16, U32, U64 Word8, Word16, Word32, Word64 Unsigned integers
I8, I16, I32, I64 Int8, Int16, Int32, Int64 Signed integers
F32, F64 Float, Double Floating point
Bool Bool Direct mapping
List a [a] Lists
Dict k v Map k v Use Data.Map
Set a Set a Use Data.Set
Result a e Either e a Note reversed type params
[Tag1, Tag2] data X = Tag1 | Tag2 Sum types
{ field : Type } data X = X { field :: Type } Records
Task a err IO a or ExceptT err IO a Effects
where a implements Ability (TypeClass a) => Constraints

When Converting Code

  1. Analyze platform boundaries - Understand where Roc platform ends and application begins
  2. Map types first - Roc's structural types need explicit Haskell ADTs
  3. Preserve semantics over syntax similarity
  4. Embrace laziness - Haskell is lazy by default; Roc is strict
  5. Handle effects properly - Roc Tasks become IO or monad transformers
  6. Test equivalence - Same inputs → same outputs for pure logic

Type System Mapping

Primitive Types

Roc Haskell Notes
Str String List of Char (less efficient)
Str Text Preferred from Data.Text
U8 Word8 From Data.Word
U16 Word16 From Data.Word
U32 Word32 From Data.Word
U64 Word64 From Data.Word
U128 Integer No direct u128, use arbitrary precision
I8 Int8 From Data.Int
I16 Int16 From Data.Int
I32 Int32 From Data.Int
I64 Int64 From Data.Int
I128 Integer Arbitrary precision
F32 Float 32-bit float
F64 Double 64-bit float
Bool Bool Direct mapping
Num a Polymorphic number Use type classes

Collection Types

Roc Haskell Notes
List a [a] Linked list
List a Vector a For indexed access (Data.Vector)
Dict k v Map k v From Data.Map
Set a Set a From Data.Set

Composite Types

Roc Haskell Notes
{ name : Str, age : U32 } data User = User { name :: Text, age :: Word32 } Record syntax
[Ok a, Err e] Either e a Note: type params reversed!
[Some a, None] Maybe a Optional values
[Tag1, Tag2, Tag3] data X = Tag1 | Tag2 | Tag3 Sum types
[Tag(T)] data X = Tag T Tag with payload

Function Types

Roc Haskell Notes
a -> b a -> b Simple function
a, b -> c a -> b -> c Curried by default
a -> b where a implements Eq (Eq a) => a -> b Type class constraint

Idiom Translation

Pattern 1: Records and Record Updates

Roc:

user = { name: "Alice", age: 30, email: "alice@example.com" }

# Update syntax
olderUser = { user & age: 31 }

# Field access
userName = user.name

Haskell:

data User = User
    { name :: Text
    , age :: Word32
    , email :: Text
    } deriving (Show, Eq)

user = User "Alice" 30 "alice@example.com"

-- Update syntax
olderUser = user { age = 31 }

-- Field access (auto-generated accessor functions)
userName = name user

Why this translation:

  • Roc's anonymous records need named data declarations in Haskell
  • Both support record update syntax, but Haskell generates accessor functions
  • Haskell requires explicit type declarations; Roc infers structural types

Pattern 2: Tag Unions and Pattern Matching

Roc:

# Tag union
Color : [Red, Yellow, Green, Custom(U8, U8, U8)]

# Pattern matching
colorName = when color is
    Red -> "red"
    Yellow -> "yellow"
    Green -> "green"
    Custom(r, g, b) -> "rgb(\(Num.toStr(r)), \(Num.toStr(g)), \(Num.toStr(b)))"

Haskell:

-- Algebraic data type
data Color
    = Red
    | Yellow
    | Green
    | Custom Word8 Word8 Word8
    deriving (Show, Eq)

-- Pattern matching with case
colorName :: Color -> String
colorName color = case color of
    Red -> "red"
    Yellow -> "yellow"
    Green -> "green"
    Custom r g b -> "rgb(" ++ show r ++ ", " ++ show g ++ ", " ++ show b ++ ")"

-- Or with function patterns
colorName' :: Color -> String
colorName' Red = "red"
colorName' Yellow = "yellow"
colorName' Green = "green"
colorName' (Custom r g b) = "rgb(" ++ show r ++ ", " ++ show g ++ ", " ++ show b ++ ")"

Why this translation:

  • Roc's structural tag unions become nominal ADTs in Haskell
  • Both enforce exhaustive pattern matching
  • Haskell allows pattern matching in function definitions, not just case expressions

Pattern 3: Result Type and Error Handling

Roc:

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

# Using try (!) for propagation
calculate : I64, I64, I64 -> Result I64 [DivByZero]
calculate = \a, b, c ->
    x = divide!(a, b)
    y = divide!(x, c)
    Ok(y)

Haskell:

-- Either for errors (note reversed params from Roc)
data DivError = DivByZero deriving (Show, Eq)

divide :: Int64 -> Int64 -> Either DivError Int64
divide a 0 = Left DivByZero
divide a b = Right (a `div` b)

-- Using do-notation for propagation
calculate :: Int64 -> Int64 -> Int64 -> Either DivError Int64
calculate a b c = do
    x <- divide a b
    y <- divide x c
    return y

-- Or with applicative style
calculate' :: Int64 -> Int64 -> Int64 -> Either DivError Int64
calculate' a b c =
    divide a b >>= \x ->
    divide x c

Why this translation:

  • Roc's Result a e maps to Haskell's Either e a (type params reversed!)
  • Roc's ! suffix maps to Haskell's <- in do-notation
  • Both provide monadic error propagation
  • Haskell's Either is more general (any error type), Roc uses tag unions

Pattern 4: Abilities to Type Classes

Roc:

# Using ability constraint
toString : a -> Str where a implements Inspect
toString = \value ->
    Inspect.toStr(value)

# Custom type automatically implements abilities
User : {
    name : Str,
    age : U32,
}

user = { name: "Alice", age: 30 }
expect Inspect.toStr(user) == "{ name: \"Alice\", age: 30 }"

Haskell:

-- Type class constraint
toString :: (Show a) => a -> String
toString value = show value

-- Custom type with deriving
data User = User
    { name :: Text
    , age :: Word32
    } deriving (Show, Eq)

user = User "Alice" 30
-- show user == "User {name = \"Alice\", age = 30}"

Why this translation:

  • Roc abilities are similar to Haskell type classes
  • Roc auto-derives abilities; Haskell requires explicit deriving clauses
  • Haskell has more established type classes (Functor, Monad, etc.)

Pattern 5: List Pipeline Operations

Roc:

numbers = [1, 2, 3, 4, 5]

result = numbers
    |> List.map(\n -> n * 2)
    |> List.keepIf(\n -> n > 5)
    |> List.walk(0, \acc, n -> acc + n)

Haskell:

import Data.Function ((&))

numbers = [1, 2, 3, 4, 5]

-- Using function composition (right to left)
result = foldr (+) 0 . filter (>5) . map (*2) $ numbers

-- Or using & operator (left to right, like Roc)
result' = numbers
    & map (*2)
    & filter (>5)
    & foldr (+) 0

Why this translation:

  • Roc's |> maps to Haskell's & operator
  • Function composition . is more idiomatic but reads backward
  • Haskell's foldr/foldl map to Roc's List.walk

Error Handling

Result → Either Translation

Type Mapping:

Roc:     Result a e
         [Ok a, Err e]

Haskell: Either e a
         Left e | Right a

Key Difference: Type parameters are reversed!

Roc:

parseAge : Str -> Result U32 [ParseError Str]
parseAge = \str ->
    when Str.toU32(str) is
        Ok(n) -> Ok(n)
        Err(_) -> Err(ParseError("Not a valid number"))

# Chain operations
validateUser : Str, Str -> Result User [ParseError Str, InvalidEmail]
validateUser = \ageStr, emailStr ->
    age = parseAge!(ageStr)
    email = validateEmail!(emailStr)
    Ok({ name: "User", age, email })

Haskell:

import Text.Read (readMaybe)
import Data.Text (Text)

data ValidationError
    = ParseError String
    | InvalidEmail
    deriving (Show, Eq)

parseAge :: Text -> Either ValidationError Word32
parseAge str =
    case readMaybe (unpack str) of
        Just n -> Right n
        Nothing -> Left (ParseError "Not a valid number")

-- Chain with do-notation
validateUser :: Text -> Text -> Either ValidationError User
validateUser ageStr emailStr = do
    age <- parseAge ageStr
    email <- validateEmail emailStr
    return $ User "User" age email

Multiple Error Types

Roc:

# Tag union for multiple errors
Error : [ParseError Str, DivByZero, NetworkError Str]

process : Str, Str -> Result I64 Error
process = \aStr, bStr ->
    a = Str.toI64!(aStr) |> Result.mapErr(\_ -> ParseError("Invalid a"))
    b = Str.toI64!(bStr) |> Result.mapErr(\_ -> ParseError("Invalid b"))
    divide!(a, b) |> Result.mapErr(\_ -> DivByZero)

Haskell:

data Error
    = ParseError String
    | DivByZero
    | NetworkError String
    deriving (Show, Eq)

process :: Text -> Text -> Either Error Int64
process aStr bStr = do
    a <- parseI64 aStr `mapLeft` const (ParseError "Invalid a")
    b <- parseI64 bStr `mapLeft` const (ParseError "Invalid b")
    divide a b `mapLeft` const DivByZero
  where
    mapLeft f (Left e) = Left (f e)
    mapLeft _ (Right x) = Right x

Platform Model Translation

Roc Platform/Application → Haskell IO

Roc Application:

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

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

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

Haskell:

import qualified Data.Text.IO as TIO
import qualified Data.Text as T
import System.IO

main :: IO ()
main = do
    content <- TIO.readFile "input.txt"
    let processed = T.toUpper content
    TIO.writeFile "output.txt" processed
    putStrLn "Done!"

Key Differences:

  • Roc separates platform (I/O) from application (pure code)
  • Haskell uses IO monad throughout
  • Roc's ! suffix maps to Haskell's <- in do-notation
  • Both use monadic composition for effects

Task Error Handling

Roc:

readConfig : Str -> Task Config [FileNotFound, ParseError Str]
readConfig = \path ->
    content = File.readUtf8!(path)  # May fail with FileNotFound
    when parseJson(content) is
        Ok(config) -> Task.ok(config)
        Err(e) -> Task.err(ParseError(e))

Haskell:

import Control.Monad.Except
import qualified Data.Text.IO as TIO

data ConfigError
    = FileNotFound
    | ParseError String
    deriving (Show, Eq)

readConfig :: FilePath -> ExceptT ConfigError IO Config
readConfig path = do
    contentE <- liftIO $ try $ TIO.readFile path
    content <- case contentE of
        Left (_ :: IOException) -> throwError FileNotFound
        Right c -> return c
    case parseJson content of
        Left e -> throwError (ParseError e)
        Right config -> return config

Why this translation:

  • Roc Tasks with error types map to ExceptT err IO a
  • Both provide error propagation and recovery
  • Haskell separates IO errors (exceptions) from domain errors (Either/ExceptT)

Concurrency Patterns

Roc Task Concurrency → Haskell Async

Roc:

# Platform-provided concurrency
import pf.Task exposing [Task]
import pf.Http

fetchBoth : Task (Str, Str) [HttpErr]
fetchBoth =
    # Platform may execute concurrently
    Task.parallel2(
        Http.get("http://api.example.com/1"),
        Http.get("http://api.example.com/2")
    )

Haskell:

import Control.Concurrent.Async
import Network.HTTP.Simple

fetchBoth :: IO (ByteString, ByteString)
fetchBoth = concurrently
    (getResponseBody <$> httpBS "http://api.example.com/1")
    (getResponseBody <$> httpBS "http://api.example.com/2")

-- Or with race (first wins)
fetchFirst :: IO ByteString
fetchFirst = race
    (getResponseBody <$> httpBS "http://api.example.com/1")
    (getResponseBody <$> httpBS "http://api.example.com/2")
    >>= either return return

Why this translation:

  • Roc delegates all concurrency to the platform
  • Haskell provides explicit concurrency primitives (async library)
  • Both provide structured concurrency with proper cleanup

No Direct Threading

Roc:

# Roc applications don't directly manage threads
# All concurrency is platform capability
# Task composition is the only abstraction

Haskell:

import Control.Concurrent

-- Haskell provides lightweight threads
main = do
    forkIO $ do
        threadDelay 1000000
        putStrLn "Hello from thread"
    putStrLn "Main thread continues"
    threadDelay 2000000

-- Use async for structured concurrency (recommended)
import Control.Concurrent.Async

main' = do
    a <- async $ do
        threadDelay 1000000
        return "result"
    result <- wait a
    print result

Laziness Translation

Roc (Strict) → Haskell (Lazy)

Roc:

# Roc is strict by default
numbers = [1, 2, 3, 4, 5]

# This evaluates immediately
doubled = List.map(numbers, \n -> n * 2)

# Infinite lists require Stream or explicit laziness
naturals = Stream.iterate(0, \n -> n + 1)

Haskell:

-- Haskell is lazy by default
numbers = [1, 2, 3, 4, 5]

-- This creates a thunk, evaluated on demand
doubled = map (*2) numbers

-- Infinite lists work naturally
naturals = iterate (+1) 0
take 10 naturals  -- [0,1,2,3,4,5,6,7,8,9]

-- Force strict evaluation when needed
import Control.DeepSeq

strictDoubled = force $ map (*2) numbers

Key Differences:

  • Roc evaluates eagerly; add explicit limits before processing
  • Haskell evaluates lazily; infinite structures work out of the box
  • When converting, be careful with space leaks in Haskell (use seq, $!, or strict data structures)

Common Pitfalls

1. Result Type Parameter Order

❌ Assuming Roc Result and Haskell Either have same param order
✓ Remember: Result a e → Either e a (reversed!)

Roc:

divide : I64, I64 -> Result I64 [DivByZero]
#                    Result ^ok  ^err

Haskell:

divide :: Int64 -> Int64 -> Either DivError Int64
--                          Either ^err     ^ok

2. Structural vs Nominal Types

❌ Using Haskell tuples for Roc records
✓ Define proper ADTs with record syntax

Roc:

user = { name: "Alice", age: 30 }  # Structural type

Haskell:

-- Wrong: (Text, Word32)  -- Positional, no field names
-- Right:
data User = User { name :: Text, age :: Word32 }

3. Platform Boundary Confusion

❌ Trying to replicate Roc's platform model in Haskell
✓ Use IO monad or mtl transformers for effects

4. Strict vs Lazy Semantics

❌ Assuming Roc's eager evaluation in Haskell
✓ Add explicit limits (take, drop) before consuming infinite lists
✓ Use strict variants when performance matters (foldl', force)

5. Ability Auto-Derivation

❌ Expecting Haskell to auto-derive like Roc
✓ Add explicit deriving clauses (Show, Eq, etc.)

Tooling

Tool Purpose Notes
GHC Haskell compiler Primary compiler
GHCi REPL Interactive development
Stack Build tool Dependency management, reproducible builds
Cabal Build tool Alternative to Stack
HLint Linter Code suggestions
Ormolu / Brittany Formatter Code formatting
hspec / QuickCheck Testing Unit and property-based tests

No direct Roc→Haskell transpiler exists; conversion is manual.


Examples

Example 1: Simple - Tag Union Pattern Matching

Before (Roc):

Status : [Pending, Approved, Rejected]

handleStatus : Status -> Str
handleStatus = \status ->
    when status is
        Pending -> "Waiting..."
        Approved -> "Done!"
        Rejected -> "Failed"

After (Haskell):

data Status = Pending | Approved | Rejected
    deriving (Show, Eq)

handleStatus :: Status -> String
handleStatus Pending = "Waiting..."
handleStatus Approved = "Done!"
handleStatus Rejected = "Failed"

Example 2: Medium - Result with Error Propagation

Before (Roc):

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

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

# Usage
when calculate(20, 4, 2) is
    Ok(result) -> Num.toStr(result)
    Err(DivByZero) -> "Error: division by zero"

After (Haskell):

data DivError = DivByZero
    deriving (Show, Eq)

divide :: Int64 -> Int64 -> Either DivError Int64
divide _ 0 = Left DivByZero
divide a b = Right (a `div` b)

calculate :: Int64 -> Int64 -> Int64 -> Either DivError Int64
calculate a b c = do
    x <- divide a b
    y <- divide x c
    return y

-- Usage
result = case calculate 20 4 2 of
    Right r -> show r
    Left DivByZero -> "Error: division by zero"

Example 3: Complex - Platform Application to IO

Before (Roc):

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

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

Config : { port : U16, host : Str }

parseConfig : Str -> Result Config [ParseError Str]
parseConfig = \content ->
    # Parse JSON content
    when Json.decode(content) is
        Ok(config) -> Ok(config)
        Err(e) -> Err(ParseError(e))

main : Task {} []
main =
    # Read config file
    content = File.readUtf8!("config.json")

    # Parse config
    config = when parseConfig(content) is
        Ok(c) -> Task.ok!(c)
        Err(ParseError(msg)) ->
            Stdout.line!("Config error: \(msg)")
            Task.err!(ConfigError)

    # Use config
    Stdout.line!("Starting server on \(config.host):\(Num.toStr(config.port))")

After (Haskell):

{-# LANGUAGE DeriveGeneric #-}

import qualified Data.Text.IO as TIO
import qualified Data.Text as T
import Data.Aeson (FromJSON, eitherDecode)
import GHC.Generics
import Control.Monad.Except
import qualified Data.ByteString.Lazy as BL

data Config = Config
    { port :: Word16
    , host :: Text
    } deriving (Generic, Show)

instance FromJSON Config

data AppError
    = ParseError String
    | ConfigError
    deriving (Show, Eq)

parseConfig :: BL.ByteString -> Either AppError Config
parseConfig content =
    case eitherDecode content of
        Right config -> Right config
        Left err -> Left (ParseError err)

main :: IO ()
main = do
    result <- runExceptT $ do
        -- Read config file
        content <- liftIO $ BL.readFile "config.json"

        -- Parse config
        config <- case parseConfig content of
            Right c -> return c
            Left (ParseError msg) -> do
                liftIO $ putStrLn $ "Config error: " ++ msg
                throwError ConfigError

        -- Use config
        liftIO $ putStrLn $
            "Starting server on " ++ T.unpack (host config)
            ++ ":" ++ show (port config)

    case result of
        Left err -> putStrLn $ "Error: " ++ show err
        Right _ -> return ()

See Also

For more examples and patterns, see:

  • meta-convert-dev - Foundational patterns with cross-language examples
  • convert-elm-haskell - Similar pure functional language conversion
  • lang-roc-dev - Roc development patterns
  • lang-haskell-dev - Haskell development patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev - Compare Roc Task model with Haskell async/STM
  • patterns-serialization-dev - JSON handling across languages
  • patterns-metaprogramming-dev - Template Haskell vs no metaprogramming in Roc