Claude-skill-registry convert-haskell-roc
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.
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-haskell-roc" ~/.claude/skills/majiayu000-claude-skill-registry-convert-haskell-roc && rm -rf "$T"
skills/data/convert-haskell-roc/SKILL.mdConvert 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
- 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: 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 |
|---|---|---|
<br> | <br> | Function definition |
| | String type |
/ | / | Integer types (Roc fixed-size) |
/ | / | Floating point |
| | Boolean type |
/ | / | Optional values via tag unions |
| | Lists (Roc is strict, not lazy) |
| | Tuples (same syntax) |
| Record or tag union | Depends on usage |
| | Optional pattern |
| | Error handling (note reversed order) |
| | Effects via platform |
| Ability constraint | Type classes → abilities |
| | Pattern matching |
notation | suffix for tasks | Monadic sequencing |
When Converting Code
- Analyze source thoroughly - understand lazy semantics before converting
- Map types first - convert type classes to ability constraints
- Identify strict vs lazy - translate infinite lists to finite or iterators
- Preserve semantics over syntax similarity
- Adopt platform model - separate pure logic from I/O via platform boundary
- Handle monads explicitly - IO → Task, Maybe → tag union, Either → Result
- Test equivalence - same inputs → same outputs (watch for strictness differences)
- Leverage abilities - replace type class constraints with ability constraints
Type System Mapping
Primitive Types
| Haskell | Roc | Notes |
|---|---|---|
| | 64-bit signed (platform-dependent in Haskell) |
| N/A | Arbitrary precision - use fixed size or external library |
| | 64-bit floating point |
| | 32-bit floating point |
| | Direct mapping |
| | Unicode code point |
| | Unit type |
| | String type |
Important differences:
- Haskell: Arbitrary precision
, lazy evaluationInteger - Roc: Fixed-size integers, strict evaluation
- Haskell:
isString
(linked list), lazy[Char] - Roc:
is UTF-8 byte array, strictStr
Collection Types
| Haskell | Roc | Notes |
|---|---|---|
| | LAZY in Haskell, STRICT in Roc |
| | Tuples (same syntax) |
| | N-tuples |
| | Dictionaries (requires + abilities) |
| | Sets (requires + 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 |
|---|---|---|
| | Product type → record |
| | Sum type → tag union |
| | Newtype → opaque type |
| | 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
maps to RocEither e a
(note reversed order!)Result a e - Haskell's
notation maps to Roc'sdo
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
monad maps to Roc'sIO
typeTask - Platform provides I/O primitives (Stdout, File, etc.)
- No explicit
- usereturn
for wrapping pure valuesTask.ok
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
maps to RocShowInspect - 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:
notation for any monaddo - 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 | compiler | Compiles to native or LLVM IR |
| GHCi (REPL) | | Interactive REPL |
| Stack / Cabal | Platforms | Dependency management via platforms |
| HSpec / Tasty | | Built-in testing with expect |
| QuickCheck | N/A | No built-in property testing |
| hlint | N/A | No Roc linter yet |
| Hoogle | | 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:
- Zero/Default Values: Roc has optional fields via tag unions, but no comprehensive Default trait equivalent
- Serialization Idioms: Encode/Decode abilities mentioned but lacks practical examples (JSON, YAML)
- Build/Deps: Package structure shown but no
,roc build
,roc test
command documentationroc run
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:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Dynamic FP to static FP (similar paradigm shift)convert-clojure-roc
- Haskell development patternslang-haskell-dev
- Roc development patternslang-roc-dev
Cross-cutting pattern skills:
- STM vs Task models across languagespatterns-concurrency-dev
- JSON, YAML serialization patternspatterns-serialization-dev
- Type classes vs abilities comparisonpatterns-metaprogramming-dev