--- 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`
`f x = ...` | `f : a -> b`
`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/)