home / skills / arustydev / ai / convert-haskell-roc
/components/skills/convert-haskell-roc
This skill helps migrate Haskell code to Roc by mapping types, translating idioms, and aligning with platform architecture for robust conversions.
npx playbooks add skill arustydev/ai --skill convert-haskell-rocReview the files below or copy the command above to add this skill to your agents.
---
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`<br>`f x = ...` | `f : a -> b`<br>`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
-- Haskell: Infinite list (lazy)
naturals :: [Integer]
naturals = [0..]
take 10 naturals -- [0,1,2,3,4,5,6,7,8,9]
```
```roc
# 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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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):**
```haskell
-- 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):**
```roc
# 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:**
```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:**
```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:**
```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:**
```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
-- Haskell: Works fine
naturals = [0..]
evens = filter even naturals
```
```roc
# 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
```roc
# 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
-- Haskell: Polymorphic function
sort :: Ord a => [a] -> [a]
sort = ...
```
```roc
# 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
```roc
# 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
-- Haskell: IO monad isolates effects
main :: IO ()
main = do
content <- readFile "config.txt" -- IO
let result = process content -- Pure
print result -- IO
```
```roc
# 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
-- Haskell
parseUser :: String -> Either String User
parseUser str = do
age <- parseAge str
email <- parseEmail str
return $ User email age
```
```roc
# 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
-- Haskell: Polymorphic
id x = x -- Inferred: a -> a
```
```roc
# 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):**
```haskell
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):**
```roc
# 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):**
```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):**
```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):**
```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):**
```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):**
```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):**
```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
- [Roc Tutorial](https://www.roc-lang.org/tutorial)
- [Roc vs Haskell Comparison](https://www.roc-lang.org/faq.html#how-does-roc-compare-to-haskell)
- [Haskell to Roc Migration Guide](https://github.com/roc-lang/roc/wiki/Haskell-to-Roc)
- [Learn You a Haskell](http://learnyouahaskell.com/)
- [Real World Haskell](http://book.realworldhaskell.org/)
This skill converts Haskell code into idiomatic Roc, focusing on semantic-preserving translations from lazy, pure functional programs to Roc's strict, platform-oriented architecture. It provides Haskell-to-Roc type mappings, idiom translations (type classes → abilities, monads → Tasks/Results), and conversion patterns for evaluation and concurrency. Use it when migrating applications or refactoring Haskell designs to Roc's platform model.
The skill analyzes Haskell source patterns (types, monadic flows, lazy structures, and concurrency primitives) and proposes Roc replacements: structural types, tag unions, Tasks, and ability constraints. It maps common Haskell idioms to Roc equivalents, flags lazy-to-strict pitfalls (infinite lists, cyclic definitions), and suggests platform-managed strategies for I/O and concurrency. The output is a set of concrete code templates, type mappings, and transformation recommendations to preserve semantics while adopting Roc conventions.
Does this skill fully automate conversion of all Haskell features?
No. It handles common patterns and idioms but excludes advanced type system features like GADTs, Type Families, and DataKinds; those require manual design decisions.
How are Haskell monads translated?
Effectful monads like IO map to Task, Maybe maps to tag unions [Some, None], and Either maps to Result with order Result value error.
What about lazy infinite structures?
Roc is strict, so convert infinite lists to finite generators, stream/iterator patterns, or platform-provided streaming APIs to preserve behavior safely.