xiaohongshu
xiaohongshu
convert-haskell-roc
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 spec...
Full skill instructions
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.
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.
meta-convert-devlang-haskell-devlang-roc-devconvert-roc-haskell| 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 |
| 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:
Integer, lazy evaluationString is [Char] (linked list), lazyStr is UTF-8 byte array, strict| 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
| 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 |
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:
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:
Either e a maps to Roc Result a e (note reversed order!)do notation maps to Roc's ! try operatorHaskell:
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:
IO monad maps to Roc's Task typereturn - use Task.ok for wrapping pure values! suffix for task sequencing (like Haskell's <-)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:
Show maps to Roc InspectHaskell:
-- 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:
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:
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:
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:
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)
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)
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:
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:
do notation for any monad! operator only for Result and TaskProblem: 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
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:
| 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 |
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"
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 }
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)"
The following areas required external research due to incomplete coverage in lang-roc-dev:
roc build, roc test, roc run command documentationThese gaps have been addressed in this skill through:
See issues #XXX, #YYY, #ZZZ for tracking improvements to lang-roc-dev.
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 patternsCross-cutting pattern skills:
patterns-concurrency-dev - STM vs Task models across languagespatterns-serialization-dev - JSON, YAML serialization patternspatterns-metaprogramming-dev - Type classes vs abilities comparisonxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience