Claude Code Plugins

Community-maintained marketplace

Feedback

convert-haskell-roc

@aRustyDev/ai
0
0

Convert Haskell code to idiomatic Roc. Use when migrating Haskell applications to Roc's platform model, translating lazy pure functional code to strict platform-based architecture, or refactoring type class based designs to ability-based patterns. Extends meta-convert-dev with Haskell-to-Roc specific patterns.

Install Skill

1Download skill
2Enable skills in Claude

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

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

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

SKILL.md

name convert-haskell-roc
description Convert Haskell code to idiomatic Roc. Use when migrating Haskell applications to Roc's platform model, translating lazy pure functional code to strict platform-based architecture, or refactoring type class based designs to ability-based patterns. Extends meta-convert-dev with Haskell-to-Roc specific patterns.

Convert Haskell to Roc

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

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: Haskell's HM types → Roc's structural types
  • Idiom translations: Type classes → abilities, monads → platform effects
  • Error handling: Maybe/Either → Result with tag unions
  • Evaluation strategy: Lazy → strict evaluation
  • Concurrency patterns: STM/async → platform-managed tasks
  • Platform architecture: GHC runtime → platform/application separation
  • Paradigm shift: Pure lazy functional → strict functional with platform effects

This Skill Does NOT Cover

  • General conversion methodology - see meta-convert-dev
  • Haskell language fundamentals - see lang-haskell-dev
  • Roc language fundamentals - see lang-roc-dev
  • Reverse conversion (Roc → Haskell) - see convert-roc-haskell
  • Advanced type system features (GADTs, Type Families, DataKinds)

Quick Reference

Haskell Roc Notes
f :: a -> b
f x = ...
f : a -> b
f = \x -> ...
Function definition
String Str String type
Int / Integer I64 / I32 Integer types (Roc fixed-size)
Double / Float F64 / F32 Floating point
Bool Bool Boolean type
Nothing / Just a None / Some a Optional values via tag unions
[a] List a Lists (Roc is strict, not lazy)
(a, b) (a, b) Tuples (same syntax)
data Record or tag union Depends on usage
Maybe a [Some a, None] Optional pattern
Either e a Result a e Error handling (note reversed order)
IO a Task a err Effects via platform
class C a where Ability constraint Type classes → abilities
case x of when x is Pattern matching
do notation ! suffix for tasks Monadic sequencing

When Converting Code

  1. Analyze source thoroughly - understand lazy semantics before converting
  2. Map types first - convert type classes to ability constraints
  3. Identify strict vs lazy - translate infinite lists to finite or iterators
  4. Preserve semantics over syntax similarity
  5. Adopt platform model - separate pure logic from I/O via platform boundary
  6. Handle monads explicitly - IO → Task, Maybe → tag union, Either → Result
  7. Test equivalence - same inputs → same outputs (watch for strictness differences)
  8. Leverage abilities - replace type class constraints with ability constraints

Type System Mapping

Primitive Types

Haskell Roc Notes
Int I64 64-bit signed (platform-dependent in Haskell)
Integer N/A Arbitrary precision - use fixed size or external library
Double F64 64-bit floating point
Float F32 32-bit floating point
Bool Bool Direct mapping
Char U32 Unicode code point
() {} Unit type
String Str String type

Important differences:

  • Haskell: Arbitrary precision Integer, lazy evaluation
  • Roc: Fixed-size integers, strict evaluation
  • Haskell: String is [Char] (linked list), lazy
  • Roc: Str is UTF-8 byte array, strict

Collection Types

Haskell Roc Notes
[a] List a LAZY in Haskell, STRICT in Roc
(a, b) (a, b) Tuples (same syntax)
(a, b, c) (a, b, c) N-tuples
Map k v Dict k v Dictionaries (requires Hash + Eq abilities)
Set a Set a Sets (requires Hash + Eq abilities)

Lazy → Strict Conversion:

-- Haskell: Infinite list (lazy)
naturals :: [Integer]
naturals = [0..]

take 10 naturals  -- [0,1,2,3,4,5,6,7,8,9]
# Roc: Must be finite or use generator pattern
naturals : List I64
naturals = List.range { start: At 0, end: At 1000000 }

List.take naturals 10  # [0,1,2,3,4,5,6,7,8,9]

# Alternative: Iterator/Stream pattern (platform-provided)
# naturalsStream = Stream.iterate 0 (\n -> n + 1)
# Stream.take naturalsStream 10

Composite Types

Haskell Roc Notes
data Point = Point Int Int Point : { x : I64, y : I64 } Product type → record
data Shape = Circle Float | Rect Float Float Shape : [Circle F64, Rect F64 F64] Sum type → tag union
newtype Age = Age Int Age := I64 Newtype → opaque type
type Name = String Name : Str Type alias

Idiom Translation

Pattern: Maybe/Optional Values

Haskell:

findUser :: Int -> Maybe User
findUser 1 = Just (User "Alice" 30)
findUser _ = Nothing

-- Using Maybe
getUserName :: Int -> String
getUserName uid = case findUser uid of
    Just user -> name user
    Nothing -> "Unknown"

-- With do notation
getOlderUser :: Int -> Maybe User
getOlderUser uid = do
    user <- findUser uid
    return $ user { age = age user + 1 }

Roc:

findUser : I64 -> [Some User, None]
findUser = \uid ->
    if uid == 1 then
        Some { name: "Alice", age: 30 }
    else
        None

# Using pattern matching
getUserName : I64 -> Str
getUserName = \uid ->
    when findUser uid is
        Some user -> user.name
        None -> "Unknown"

# No monadic do - use direct manipulation
getOlderUser : I64 -> [Some User, None]
getOlderUser = \uid ->
    when findUser uid is
        Some user -> Some { user & age: user.age + 1 }
        None -> None

Why this translation:

  • Roc uses structural tag unions instead of Maybe type constructor
  • No monadic bind for optional values - use explicit pattern matching
  • More verbose but clearer control flow

Pattern: Either/Error Handling

Haskell:

divide :: Float -> Float -> Either String Float
divide _ 0 = Left "Division by zero"
divide x y = Right (x / y)

-- Chaining with do notation
calculate :: Float -> Float -> Float -> Either String Float
calculate a b c = do
    x <- divide a b
    y <- divide x c
    return y

-- With error mapping
parseAge :: String -> Either String Int
parseAge str = case reads str of
    [(n, "")] -> if n >= 0
                 then Right n
                 else Left "Age must be non-negative"
    _ -> Left "Not a valid number"

Roc:

divide : F64, F64 -> Result F64 [DivByZero]
divide = \x, y ->
    if y == 0 then
        Err DivByZero
    else
        Ok (x / y)

# Chaining with try operator (!)
calculate : F64, F64, F64 -> Result F64 [DivByZero]
calculate = \a, b, c ->
    x = divide! a b  # Early return on Err
    y = divide! x c
    Ok y

# With error mapping
parseAge : Str -> Result I64 [ParseError Str, InvalidAge]
parseAge = \str ->
    n = Str.toI64! str |> Result.mapErr \_ -> ParseError "Not a number"

    if n >= 0 then
        Ok n
    else
        Err InvalidAge

Why this translation:

  • Haskell Either e a maps to Roc Result a e (note reversed order!)
  • Haskell's do notation maps to Roc's ! try operator
  • Tag unions allow more expressive error types than String

Pattern: IO Monad → Task

Haskell:

main :: IO ()
main = do
    putStrLn "What is your name?"
    name <- getLine
    putStrLn $ "Hello, " ++ name

-- Reading files
readConfig :: FilePath -> IO String
readConfig path = do
    content <- readFile path
    return content

Roc:

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

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

# Reading files
import pf.File

readConfig : Str -> Task Str [FileReadErr]
readConfig = \path ->
    content = File.readUtf8! path
    Task.ok content

Why this translation:

  • Haskell's IO monad maps to Roc's Task type
  • Platform provides I/O primitives (Stdout, File, etc.)
  • No explicit return - use Task.ok for wrapping pure values
  • ! suffix for task sequencing (like Haskell's <-)

Pattern: Type Classes → Abilities

Haskell:

-- Type class definition
class Eq a where
    (==) :: a -> a -> Bool

class Show a where
    show :: a -> String

-- Using type class constraints
printEqual :: (Eq a, Show a) => a -> a -> IO ()
printEqual x y = putStrLn $ if x == y
    then show x ++ " equals " ++ show y
    else show x ++ " not equals " ++ show y

-- Deriving instances
data Color = Red | Green | Blue
    deriving (Eq, Show)

Roc:

# Abilities are automatically derived for records and tags
Color : [Red, Green, Blue]

# Ability constraints in function signatures
printEqual : a, a -> Task {} [] where a implements Eq & Inspect
printEqual = \x, y ->
    msg = if x == y then
        "\(Inspect.toStr x) equals \(Inspect.toStr y)"
    else
        "\(Inspect.toStr x) not equals \(Inspect.toStr y)"
    Stdout.line! msg

# Automatic derivation
User : {
    name : Str,
    age : U32,
}
# User automatically has: Eq, Hash, Inspect, Encode, Decode

user1 = { name: "Alice", age: 30 }
user2 = { name: "Alice", age: 30 }
user1 == user2  # Works automatically

Why this translation:

  • Haskell type classes map to Roc abilities
  • Haskell Show maps to Roc Inspect
  • Roc derives abilities automatically for records/tags
  • No manual instance definitions needed for common abilities

Pattern: Functor/Applicative/Monad → Direct Operations

Haskell:

-- Functor: fmap
doubled :: Maybe Int -> Maybe Int
doubled = fmap (*2)

-- Applicative
createUser :: Maybe String -> Maybe Int -> Maybe User
createUser mName mAge = User <$> mName <*> mAge

-- Monad: bind
chain :: Maybe Int -> Maybe Int
chain mx = mx >>= \x -> return (x * 2)

Roc:

# No Functor/Applicative/Monad abstractions
# Use explicit pattern matching or helper functions

doubled : [Some I64, None] -> [Some I64, None]
doubled = \m ->
    when m is
        Some x -> Some (x * 2)
        None -> None

# Or use Result.map for Result type
doubled = \m ->
    Result.map m \x -> x * 2

# No applicative - construct directly
createUser : [Some Str, None], [Some U32, None] -> [Some User, None]
createUser = \mName, mAge ->
    when (mName, mAge) is
        (Some name, Some age) -> Some { name, age }
        _ -> None

# Chaining
chain : [Some I64, None] -> [Some I64, None]
chain = \mx ->
    when mx is
        Some x -> Some (x * 2)
        None -> None

Why this translation:

  • Roc doesn't have Functor/Applicative/Monad abstractions
  • Use explicit pattern matching for clarity
  • Platform-specific types (Task, Result) may have helper functions
  • Simpler mental model at the cost of some verbosity

Evaluation Strategy

Lazy → Strict Translation

Haskell (Lazy):

-- Infinite Fibonacci
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

take 10 fibs  -- Only computes first 10

-- Lazy evaluation allows cycles
ones :: [Int]
ones = 1 : ones

Roc (Strict):

# Must generate finite list or use explicit generator
fibList : I64 -> List I64
fibList = \n ->
    List.walk (List.range { start: At 0, end: Before n })
        [0, 1]
        \fibs, _ ->
            a = List.get fibs (List.len fibs - 2)
                |> Result.withDefault 0
            b = List.get fibs (List.len fibs - 1)
                |> Result.withDefault 0
            List.append fibs (a + b)

fibList 10  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# Alternative: Iterator pattern (if platform provides)
# fibStream = Stream.iterate (0, 1) \(a, b) -> (b, a + b)
#             |> Stream.map \(a, _) -> a
# Stream.take fibStream 10

Key differences:

  • Haskell: Infinite structures work naturally (lazy)
  • Roc: Must use finite structures or explicit generators
  • Haskell: Evaluation on demand
  • Roc: Immediate evaluation

Concurrency Patterns

STM → Platform Tasks

Haskell:

import Control.Concurrent.STM

type Account = TVar Int

transfer :: Account -> Account -> Int -> STM ()
transfer from to amount = do
    fromBal <- readTVar from
    when (fromBal >= amount) $ do
        modifyTVar from (subtract amount)
        modifyTVar to (+ amount)

-- Run transaction
main = do
    acc1 <- newTVarIO 1000
    acc2 <- newTVarIO 0
    atomically $ transfer acc1 acc2 500

Roc:

# No built-in STM - platform manages state
# Pattern: Use platform-provided state management

import pf.Task exposing [Task]

# Platform-specific state API (example)
# This depends on your platform implementation

Account : { balance : I64 }

transfer : Account, Account, I64 -> Task {} [InsufficientFunds]
transfer = \from, to, amount ->
    if from.balance >= amount then
        # Platform handles atomicity
        newFrom = { from & balance: from.balance - amount }
        newTo = { to & balance: to.balance + amount }
        Task.ok {}
    else
        Task.err InsufficientFunds

# Usage
main : Task {} []
main =
    acc1 = { balance: 1000 }
    acc2 = { balance: 0 }
    transfer! acc1 acc2 500
    Task.ok {}

Why this translation:

  • Haskell: Built-in STM for transactional memory
  • Roc: Platform manages concurrency and state
  • Application code stays pure; platform handles atomicity
  • Platform-specific APIs vary

Async → Task-Based

Haskell:

import Control.Concurrent.Async

main :: IO ()
main = do
    (res1, res2) <- concurrently
        (fetchUrl "http://example.com/1")
        (fetchUrl "http://example.com/2")
    print (res1, res2)

Roc:

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

# Platform may provide concurrent execution
main : Task {} []
main =
    # Sequential by default
    res1 = Http.get! "http://example.com/1"
    res2 = Http.get! "http://example.com/2"

    # Or platform-provided parallel execution (if available)
    # (res1, res2) = Task.parallel2!(
    #     Http.get "http://example.com/1",
    #     Http.get "http://example.com/2"
    # )

    Stdout.line! (Inspect.toStr (res1, res2))

Why this translation:

  • Haskell: Explicit async library
  • Roc: Platform controls concurrency
  • Application code composes tasks; platform decides execution strategy

Common Pitfalls

1. Lazy vs Strict - Infinite Lists

Problem: Direct translation of lazy infinite structures

-- Haskell: Works fine
naturals = [0..]
evens = filter even naturals
# Roc: Would hang forever!
# naturals = List.range { start: At 0, end: At maxI64 }  # Too large
# evens = List.keepIf naturals Num.isEven  # Never completes

Fix: Use finite ranges or iterators

# Generate finite range
naturals = List.range { start: At 0, end: Before 1000 }
evens = List.keepIf naturals Num.isEven

# Or use stream/iterator pattern (if platform provides)

2. Type Class Constraints → Ability Constraints

Problem: Assuming type class polymorphism works the same

-- Haskell: Polymorphic function
sort :: Ord a => [a] -> [a]
sort = ...
# Roc: Ability constraint
sort : List a -> List a where a implements Ord
sort = \list -> ...

# BUT: Roc doesn't have Ord ability built-in!
# Must use specific types or platform-provided sorting

Fix: Use concrete types or platform functions

# Concrete type
sortInts : List I64 -> List I64
sortInts = List.sortAsc

# Or use platform's polymorphic sort (if available)

3. IO Monad → Task Platform Boundary

Problem: Mixing pure and impure code

-- Haskell: IO monad isolates effects
main :: IO ()
main = do
    content <- readFile "config.txt"  -- IO
    let result = process content       -- Pure
    print result                        -- IO
# Roc: Clear platform boundary
main : Task {} []
main =
    content = File.readUtf8! "config.txt"  # Task (platform)
    result = process content                # Pure function
    Stdout.line! (Inspect.toStr result)     # Task (platform)

# Pure function (no Task)
process : Str -> Str
process = \text ->
    Str.toUpper text

Key difference:

  • Haskell: IO type tracks effects
  • Roc: Platform boundary separates pure from effectful
  • Pure functions in Roc have no Task type

4. Monadic Do Notation → Try Operator

Problem: Expecting do-notation to work

-- Haskell
parseUser :: String -> Either String User
parseUser str = do
    age <- parseAge str
    email <- parseEmail str
    return $ User email age
# Roc: Use try operator (!)
parseUser : Str -> Result User [ParseErr Str]
parseUser = \str ->
    age = parseAge! str      # Early return on Err
    email = parseEmail! str  # Early return on Err
    Ok { email, age }

Key difference:

  • Haskell: do notation for any monad
  • Roc: ! operator only for Result and Task

5. Type Inference Differences

Problem: Expecting Haskell-level inference

-- Haskell: Polymorphic
id x = x  -- Inferred: a -> a
# Roc: Usually needs annotation for polymorphic functions
identity : a -> a
identity = \x -> x

# Or will infer concrete type from usage
id = \x -> x  # Type depends on how it's used

Fix: Add type signatures for polymorphic functions


Testing Strategy

Property Testing: QuickCheck → Roc Expect

Haskell (QuickCheck):

import Test.QuickCheck

prop_reverse :: [Int] -> Bool
prop_reverse xs = reverse (reverse xs) == xs

prop_sortLength :: [Int] -> Bool
prop_sortLength xs = length (sort xs) == length xs

Roc (Expect):

# Inline property-style tests
expect
    xs = [1, 2, 3, 4, 5]
    List.reverse (List.reverse xs) == xs

expect
    xs = [3, 1, 4, 1, 5, 9]
    List.len (List.sortAsc xs) == List.len xs

# For comprehensive property testing, use external fuzzer
# or generate test cases

Limitations:

  • Roc: No built-in property testing framework
  • Use expect for inline tests
  • Generate test cases externally or use platform-provided fuzzing

Tooling

Haskell Tool Roc Equivalent Notes
GHC roc compiler Compiles to native or LLVM IR
GHCi (REPL) roc repl Interactive REPL
Stack / Cabal Platforms Dependency management via platforms
HSpec / Tasty roc test Built-in testing with expect
QuickCheck N/A No built-in property testing
hlint N/A No Roc linter yet
Hoogle roc docs Generate docs from code

Examples

Example 1: Simple - Maybe to Tag Union

Before (Haskell):

data User = User { name :: String, age :: Int }

findUser :: Int -> Maybe User
findUser 1 = Just (User "Alice" 30)
findUser _ = Nothing

displayUser :: Int -> String
displayUser uid = case findUser uid of
    Just user -> "Found: " ++ name user
    Nothing -> "Not found"

After (Roc):

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

findUser : I64 -> [Some User, None]
findUser = \uid ->
    if uid == 1 then
        Some { name: "Alice", age: 30 }
    else
        None

displayUser : I64 -> Str
displayUser = \uid ->
    when findUser uid is
        Some user -> "Found: \(user.name)"
        None -> "Not found"

Example 2: Medium - Either Error Handling

Before (Haskell):

divide :: Double -> Double -> Either String Double
divide _ 0 = Left "Division by zero"
divide x y = Right (x / y)

validateAge :: Int -> Either String Int
validateAge age
    | age < 0 = Left "Age cannot be negative"
    | age > 150 = Left "Age too high"
    | otherwise = Right age

createUser :: String -> Int -> Either String User
createUser email age = do
    validAge <- validateAge age
    return $ User email validAge

After (Roc):

divide : F64, F64 -> Result F64 [DivByZero]
divide = \x, y ->
    if y == 0 then
        Err DivByZero
    else
        Ok (x / y)

validateAge : I64 -> Result I64 [NegativeAge, AgeTooHigh]
validateAge = \age ->
    if age < 0 then
        Err NegativeAge
    else if age > 150 then
        Err AgeTooHigh
    else
        Ok age

createUser : Str, I64 -> Result User [NegativeAge, AgeTooHigh]
createUser = \email, age ->
    validAge = validateAge! age
    Ok { email, age: validAge }

Example 3: Complex - IO Monad to Platform Task

Before (Haskell):

import System.IO
import Control.Exception

data Config = Config { port :: Int, host :: String }
    deriving (Show, Read)

readConfig :: FilePath -> IO (Either String Config)
readConfig path = catch
    (do
        content <- readFile path
        case reads content of
            [(config, "")] -> return $ Right config
            _ -> return $ Left "Invalid config format"
    )
    (\(e :: IOException) -> return $ Left $ show e)

runApp :: Config -> IO ()
runApp config = do
    putStrLn $ "Starting server on " ++ host config
    putStrLn $ "Port: " ++ show (port config)
    -- Actual server logic here

main :: IO ()
main = do
    result <- readConfig "config.txt"
    case result of
        Right config -> runApp config
        Left err -> putStrLn $ "Error: " ++ err

After (Roc):

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

Config : {
    port : I64,
    host : Str,
}

readConfig : Str -> Task Config [FileReadErr, InvalidFormat Str]
readConfig = \path ->
    content = File.readUtf8! path
        |> Task.mapErr \_ -> FileReadErr

    # Parse JSON or custom format
    # For simplicity, assume JSON parsing available via platform
    config = parseConfig! content
        |> Task.mapErr \_ -> InvalidFormat "Invalid config format"

    Task.ok config

parseConfig : Str -> Result Config [ParseErr]
parseConfig = \content ->
    # Parsing logic (simplified)
    # In real code, use JSON parser
    Ok { port: 8080, host: "localhost" }

runApp : Config -> Task {} []
runApp = \config ->
    Stdout.line! "Starting server on \(config.host)"
    Stdout.line! "Port: \(Num.toStr config.port)"
    # Actual server logic here
    Task.ok {}

main : Task {} []
main =
    when readConfig "config.txt" is
        Ok config -> runApp! config
        Err FileReadErr -> Stdout.line! "Error: Could not read config file"
        Err (InvalidFormat msg) -> Stdout.line! "Error: \(msg)"

Limitations (lang-roc-dev gaps)

The following areas required external research due to incomplete coverage in lang-roc-dev:

  1. Zero/Default Values: Roc has optional fields via tag unions, but no comprehensive Default trait equivalent
  2. Serialization Idioms: Encode/Decode abilities mentioned but lacks practical examples (JSON, YAML)
  3. Build/Deps: Package structure shown but no roc build, roc test, roc run command documentation

These gaps have been addressed in this skill through:

  • External Roc documentation research
  • Inference from Roc design philosophy
  • Comparison with similar languages

See issues #XXX, #YYY, #ZZZ for tracking improvements to lang-roc-dev.


See Also

For more examples and patterns, see:

  • meta-convert-dev - Foundational patterns with cross-language examples
  • convert-clojure-roc - Dynamic FP to static FP (similar paradigm shift)
  • lang-haskell-dev - Haskell development patterns
  • lang-roc-dev - Roc development patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev - STM vs Task models across languages
  • patterns-serialization-dev - JSON, YAML serialization patterns
  • patterns-metaprogramming-dev - Type classes vs abilities comparison

References