home / skills / arustydev / ai / convert-haskell-roc

convert-haskell-roc skill

/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-roc

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
23.9 KB
---
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/)

Overview

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.

How this skill works

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.

When to use it

  • Migrating a Haskell codebase to Roc's platform model
  • Refactoring type class–heavy designs into Roc abilities
  • Converting IO/monadic workflows into Task-based platform effects
  • Translating lazy data structures (infinite lists) to strict or generator patterns
  • Adapting Haskell concurrency (STM/async) to platform-managed tasks

Best practices

  • Analyze lazy semantics first — identify where laziness affects correctness or performance
  • Map types before code: convert type classes and monads to abilities and Tasks/Results
  • Replace infinite or cyclic structures with finite generators, streams, or iterators
  • Separate pure logic from platform effects; keep core functions pure and wrap I/O in Tasks
  • Write equivalence tests to catch strictness-related behavioral changes

Example use cases

  • Convert a Haskell CLI that uses IO and Maybe to a Roc Task-based CLI with explicit tag unions
  • Refactor a Haskell library that uses type classes (Eq, Show) into Roc functions with ability constraints
  • Translate lazy streaming code (infinite sequences) into explicit stream/iterator patterns on the platform
  • Migrate STM-based transactional logic to platform-managed state APIs and Task-based atomic operations
  • Port error-handling chains from Either to Roc Result with expressive tag unions and early-return (!) usage

FAQ

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.