| 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 -> bf x = ... |
f : a -> bf = \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
- 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 |
|---|---|---|
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:
Stringis[Char](linked list), lazy - Roc:
Stris 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 amaps to RocResult a e(note reversed order!) - Haskell's
donotation 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
IOmonad maps to Roc'sTasktype - Platform provides I/O primitives (Stdout, File, etc.)
- No explicit
return- useTask.okfor 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
Showmaps to RocInspect - 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:
donotation 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:
- 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,roc runcommand 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 examplesconvert-clojure-roc- Dynamic FP to static FP (similar paradigm shift)lang-haskell-dev- Haskell development patternslang-roc-dev- Roc development patterns
Cross-cutting pattern skills:
patterns-concurrency-dev- STM vs Task models across languagespatterns-serialization-dev- JSON, YAML serialization patternspatterns-metaprogramming-dev- Type classes vs abilities comparison