Bidirectional conversion between Haskell and Roc. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Haskell↔Roc specific patterns. 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 with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Bidirectional conversion between Haskell and Roc. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Haskell↔Roc specific patterns. 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.
Haskell ↔ Roc Conversion
Bidirectional conversion between Haskell and Roc. This skill extends meta-convert-dev with Haskell↔Roc specific type mappings, idiom translations, and tooling for translating from lazy pure functional programming to strict platform-based architecture.
-- 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
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
# 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 {}
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
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:
Zero/Default Values: Roc has optional fields via tag unions, but no comprehensive Default trait equivalent