xiaohongshu
xiaohongshu
convert-roc-haskell
Convert Roc code to idiomatic Haskell. Use when migrating Roc projects to Haskell, translating Roc patterns to idiomatic Haskell, or refactoring Roc codebases. Extends meta-convert-dev with Roc-to-Haskell specific patterns.
Full skill instructions
Convert Roc code to idiomatic Haskell. This skill extends meta-convert-dev with Roc-to-Haskell specific type mappings, idiom translations, and tooling for migrating platform-based Roc code to pure functional Haskell.
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-roc-devlang-haskell-devconvert-haskell-roc| Roc | Haskell | Notes |
|---|---|---|
Str | String or Text | Use Text for production |
U8, U16, U32, U64 | Word8, Word16, Word32, Word64 | Unsigned integers |
I8, I16, I32, I64 | Int8, Int16, Int32, Int64 | Signed integers |
F32, F64 | Float, Double | Floating point |
Bool | Bool | Direct mapping |
List a | [a] | Lists |
Dict k v | Map k v | Use Data.Map |
Set a | Set a | Use Data.Set |
Result a e | Either e a | Note reversed type params |
[Tag1, Tag2] | data X = Tag1 | Tag2 | Sum types |
{ field : Type } | data X = X { field :: Type } | Records |
Task a err | IO a or ExceptT err IO a | Effects |
where a implements Ability | (TypeClass a) => | Constraints |
| Roc | Haskell | Notes |
|---|---|---|
Str | String | List of Char (less efficient) |
Str | Text | Preferred from Data.Text |
U8 | Word8 | From Data.Word |
U16 | Word16 | From Data.Word |
U32 | Word32 | From Data.Word |
U64 | Word64 | From Data.Word |
U128 | Integer | No direct u128, use arbitrary precision |
I8 | Int8 | From Data.Int |
I16 | Int16 | From Data.Int |
I32 | Int32 | From Data.Int |
I64 | Int64 | From Data.Int |
I128 | Integer | Arbitrary precision |
F32 | Float | 32-bit float |
F64 | Double | 64-bit float |
Bool | Bool | Direct mapping |
Num a | Polymorphic number | Use type classes |
| Roc | Haskell | Notes |
|---|---|---|
List a | [a] | Linked list |
List a | Vector a | For indexed access (Data.Vector) |
Dict k v | Map k v | From Data.Map |
Set a | Set a | From Data.Set |
| Roc | Haskell | Notes |
|---|---|---|
{ name : Str, age : U32 } | data User = User { name :: Text, age :: Word32 } | Record syntax |
[Ok a, Err e] | Either e a | Note: type params reversed! |
[Some a, None] | Maybe a | Optional values |
[Tag1, Tag2, Tag3] | data X = Tag1 | Tag2 | Tag3 | Sum types |
[Tag(T)] | data X = Tag T | Tag with payload |
| Roc | Haskell | Notes |
|---|---|---|
a -> b | a -> b | Simple function |
a, b -> c | a -> b -> c | Curried by default |
a -> b where a implements Eq | (Eq a) => a -> b | Type class constraint |
Roc:
user = { name: "Alice", age: 30, email: "[email protected]" }
# Update syntax
olderUser = { user & age: 31 }
# Field access
userName = user.name
Haskell:
data User = User
{ name :: Text
, age :: Word32
, email :: Text
} deriving (Show, Eq)
user = User "Alice" 30 "[email protected]"
-- Update syntax
olderUser = user { age = 31 }
-- Field access (auto-generated accessor functions)
userName = name user
Why this translation:
Roc:
# Tag union
Color : [Red, Yellow, Green, Custom(U8, U8, U8)]
# Pattern matching
colorName = when color is
Red -> "red"
Yellow -> "yellow"
Green -> "green"
Custom(r, g, b) -> "rgb(\(Num.toStr(r)), \(Num.toStr(g)), \(Num.toStr(b)))"
Haskell:
-- Algebraic data type
data Color
= Red
| Yellow
| Green
| Custom Word8 Word8 Word8
deriving (Show, Eq)
-- Pattern matching with case
colorName :: Color -> String
colorName color = case color of
Red -> "red"
Yellow -> "yellow"
Green -> "green"
Custom r g b -> "rgb(" ++ show r ++ ", " ++ show g ++ ", " ++ show b ++ ")"
-- Or with function patterns
colorName' :: Color -> String
colorName' Red = "red"
colorName' Yellow = "yellow"
colorName' Green = "green"
colorName' (Custom r g b) = "rgb(" ++ show r ++ ", " ++ show g ++ ", " ++ show b ++ ")"
Why this translation:
Roc:
divide : I64, I64 -> Result I64 [DivByZero]
divide = \a, b ->
if b == 0 then
Err(DivByZero)
else
Ok(a // b)
# Using try (!) for propagation
calculate : I64, I64, I64 -> Result I64 [DivByZero]
calculate = \a, b, c ->
x = divide!(a, b)
y = divide!(x, c)
Ok(y)
Haskell:
-- Either for errors (note reversed params from Roc)
data DivError = DivByZero deriving (Show, Eq)
divide :: Int64 -> Int64 -> Either DivError Int64
divide a 0 = Left DivByZero
divide a b = Right (a `div` b)
-- Using do-notation for propagation
calculate :: Int64 -> Int64 -> Int64 -> Either DivError Int64
calculate a b c = do
x <- divide a b
y <- divide x c
return y
-- Or with applicative style
calculate' :: Int64 -> Int64 -> Int64 -> Either DivError Int64
calculate' a b c =
divide a b >>= \x ->
divide x c
Why this translation:
Result a e maps to Haskell's Either e a (type params reversed!)! suffix maps to Haskell's <- in do-notationRoc:
# Using ability constraint
toString : a -> Str where a implements Inspect
toString = \value ->
Inspect.toStr(value)
# Custom type automatically implements abilities
User : {
name : Str,
age : U32,
}
user = { name: "Alice", age: 30 }
expect Inspect.toStr(user) == "{ name: \"Alice\", age: 30 }"
Haskell:
-- Type class constraint
toString :: (Show a) => a -> String
toString value = show value
-- Custom type with deriving
data User = User
{ name :: Text
, age :: Word32
} deriving (Show, Eq)
user = User "Alice" 30
-- show user == "User {name = \"Alice\", age = 30}"
Why this translation:
deriving clausesRoc:
numbers = [1, 2, 3, 4, 5]
result = numbers
|> List.map(\n -> n * 2)
|> List.keepIf(\n -> n > 5)
|> List.walk(0, \acc, n -> acc + n)
Haskell:
import Data.Function ((&))
numbers = [1, 2, 3, 4, 5]
-- Using function composition (right to left)
result = foldr (+) 0 . filter (>5) . map (*2) $ numbers
-- Or using & operator (left to right, like Roc)
result' = numbers
& map (*2)
& filter (>5)
& foldr (+) 0
Why this translation:
|> maps to Haskell's & operator. is more idiomatic but reads backwardfoldr/foldl map to Roc's List.walkType Mapping:
Roc: Result a e
[Ok a, Err e]
Haskell: Either e a
Left e | Right a
Key Difference: Type parameters are reversed!
Roc:
parseAge : Str -> Result U32 [ParseError Str]
parseAge = \str ->
when Str.toU32(str) is
Ok(n) -> Ok(n)
Err(_) -> Err(ParseError("Not a valid number"))
# Chain operations
validateUser : Str, Str -> Result User [ParseError Str, InvalidEmail]
validateUser = \ageStr, emailStr ->
age = parseAge!(ageStr)
email = validateEmail!(emailStr)
Ok({ name: "User", age, email })
Haskell:
import Text.Read (readMaybe)
import Data.Text (Text)
data ValidationError
= ParseError String
| InvalidEmail
deriving (Show, Eq)
parseAge :: Text -> Either ValidationError Word32
parseAge str =
case readMaybe (unpack str) of
Just n -> Right n
Nothing -> Left (ParseError "Not a valid number")
-- Chain with do-notation
validateUser :: Text -> Text -> Either ValidationError User
validateUser ageStr emailStr = do
age <- parseAge ageStr
email <- validateEmail emailStr
return $ User "User" age email
Roc:
# Tag union for multiple errors
Error : [ParseError Str, DivByZero, NetworkError Str]
process : Str, Str -> Result I64 Error
process = \aStr, bStr ->
a = Str.toI64!(aStr) |> Result.mapErr(\_ -> ParseError("Invalid a"))
b = Str.toI64!(bStr) |> Result.mapErr(\_ -> ParseError("Invalid b"))
divide!(a, b) |> Result.mapErr(\_ -> DivByZero)
Haskell:
data Error
= ParseError String
| DivByZero
| NetworkError String
deriving (Show, Eq)
process :: Text -> Text -> Either Error Int64
process aStr bStr = do
a <- parseI64 aStr `mapLeft` const (ParseError "Invalid a")
b <- parseI64 bStr `mapLeft` const (ParseError "Invalid b")
divide a b `mapLeft` const DivByZero
where
mapLeft f (Left e) = Left (f e)
mapLeft _ (Right x) = Right x
Roc Application:
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/..."
}
import pf.Stdout
import pf.Task exposing [Task]
import pf.File
main : Task {} []
main =
content = File.readUtf8!("input.txt")
processed = Str.toUpper(content)
File.writeUtf8!("output.txt", processed)
Stdout.line!("Done!")
Haskell:
import qualified Data.Text.IO as TIO
import qualified Data.Text as T
import System.IO
main :: IO ()
main = do
content <- TIO.readFile "input.txt"
let processed = T.toUpper content
TIO.writeFile "output.txt" processed
putStrLn "Done!"
Key Differences:
! suffix maps to Haskell's <- in do-notationRoc:
readConfig : Str -> Task Config [FileNotFound, ParseError Str]
readConfig = \path ->
content = File.readUtf8!(path) # May fail with FileNotFound
when parseJson(content) is
Ok(config) -> Task.ok(config)
Err(e) -> Task.err(ParseError(e))
Haskell:
import Control.Monad.Except
import qualified Data.Text.IO as TIO
data ConfigError
= FileNotFound
| ParseError String
deriving (Show, Eq)
readConfig :: FilePath -> ExceptT ConfigError IO Config
readConfig path = do
contentE <- liftIO $ try $ TIO.readFile path
content <- case contentE of
Left (_ :: IOException) -> throwError FileNotFound
Right c -> return c
case parseJson content of
Left e -> throwError (ParseError e)
Right config -> return config
Why this translation:
ExceptT err IO aRoc:
# Platform-provided concurrency
import pf.Task exposing [Task]
import pf.Http
fetchBoth : Task (Str, Str) [HttpErr]
fetchBoth =
# Platform may execute concurrently
Task.parallel2(
Http.get("http://api.example.com/1"),
Http.get("http://api.example.com/2")
)
Haskell:
import Control.Concurrent.Async
import Network.HTTP.Simple
fetchBoth :: IO (ByteString, ByteString)
fetchBoth = concurrently
(getResponseBody <$> httpBS "http://api.example.com/1")
(getResponseBody <$> httpBS "http://api.example.com/2")
-- Or with race (first wins)
fetchFirst :: IO ByteString
fetchFirst = race
(getResponseBody <$> httpBS "http://api.example.com/1")
(getResponseBody <$> httpBS "http://api.example.com/2")
>>= either return return
Why this translation:
Roc:
# Roc applications don't directly manage threads
# All concurrency is platform capability
# Task composition is the only abstraction
Haskell:
import Control.Concurrent
-- Haskell provides lightweight threads
main = do
forkIO $ do
threadDelay 1000000
putStrLn "Hello from thread"
putStrLn "Main thread continues"
threadDelay 2000000
-- Use async for structured concurrency (recommended)
import Control.Concurrent.Async
main' = do
a <- async $ do
threadDelay 1000000
return "result"
result <- wait a
print result
Roc:
# Roc is strict by default
numbers = [1, 2, 3, 4, 5]
# This evaluates immediately
doubled = List.map(numbers, \n -> n * 2)
# Infinite lists require Stream or explicit laziness
naturals = Stream.iterate(0, \n -> n + 1)
Haskell:
-- Haskell is lazy by default
numbers = [1, 2, 3, 4, 5]
-- This creates a thunk, evaluated on demand
doubled = map (*2) numbers
-- Infinite lists work naturally
naturals = iterate (+1) 0
take 10 naturals -- [0,1,2,3,4,5,6,7,8,9]
-- Force strict evaluation when needed
import Control.DeepSeq
strictDoubled = force $ map (*2) numbers
Key Differences:
seq, $!, or strict data structures)❌ Assuming Roc Result and Haskell Either have same param order
✓ Remember: Result a e → Either e a (reversed!)
Roc:
divide : I64, I64 -> Result I64 [DivByZero]
# Result ^ok ^err
Haskell:
divide :: Int64 -> Int64 -> Either DivError Int64
-- Either ^err ^ok
❌ Using Haskell tuples for Roc records
✓ Define proper ADTs with record syntax
Roc:
user = { name: "Alice", age: 30 } # Structural type
Haskell:
-- Wrong: (Text, Word32) -- Positional, no field names
-- Right:
data User = User { name :: Text, age :: Word32 }
❌ Trying to replicate Roc's platform model in Haskell
✓ Use IO monad or mtl transformers for effects
❌ Assuming Roc's eager evaluation in Haskell
✓ Add explicit limits (take, drop) before consuming infinite lists
✓ Use strict variants when performance matters (foldl', force)
❌ Expecting Haskell to auto-derive like Roc
✓ Add explicit deriving clauses (Show, Eq, etc.)
| Tool | Purpose | Notes |
|---|---|---|
| GHC | Haskell compiler | Primary compiler |
| GHCi | REPL | Interactive development |
| Stack | Build tool | Dependency management, reproducible builds |
| Cabal | Build tool | Alternative to Stack |
| HLint | Linter | Code suggestions |
| Ormolu / Brittany | Formatter | Code formatting |
| hspec / QuickCheck | Testing | Unit and property-based tests |
No direct Roc→Haskell transpiler exists; conversion is manual.
Before (Roc):
Status : [Pending, Approved, Rejected]
handleStatus : Status -> Str
handleStatus = \status ->
when status is
Pending -> "Waiting..."
Approved -> "Done!"
Rejected -> "Failed"
After (Haskell):
data Status = Pending | Approved | Rejected
deriving (Show, Eq)
handleStatus :: Status -> String
handleStatus Pending = "Waiting..."
handleStatus Approved = "Done!"
handleStatus Rejected = "Failed"
Before (Roc):
divide : I64, I64 -> Result I64 [DivByZero]
divide = \a, b ->
if b == 0 then
Err(DivByZero)
else
Ok(a // b)
calculate : I64, I64, I64 -> Result I64 [DivByZero]
calculate = \a, b, c ->
x = divide!(a, b)
y = divide!(x, c)
Ok(y)
# Usage
when calculate(20, 4, 2) is
Ok(result) -> Num.toStr(result)
Err(DivByZero) -> "Error: division by zero"
After (Haskell):
data DivError = DivByZero
deriving (Show, Eq)
divide :: Int64 -> Int64 -> Either DivError Int64
divide _ 0 = Left DivByZero
divide a b = Right (a `div` b)
calculate :: Int64 -> Int64 -> Int64 -> Either DivError Int64
calculate a b c = do
x <- divide a b
y <- divide x c
return y
-- Usage
result = case calculate 20 4 2 of
Right r -> show r
Left DivByZero -> "Error: division by zero"
Before (Roc):
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/..."
}
import pf.Stdout
import pf.File
import pf.Task exposing [Task]
Config : { port : U16, host : Str }
parseConfig : Str -> Result Config [ParseError Str]
parseConfig = \content ->
# Parse JSON content
when Json.decode(content) is
Ok(config) -> Ok(config)
Err(e) -> Err(ParseError(e))
main : Task {} []
main =
# Read config file
content = File.readUtf8!("config.json")
# Parse config
config = when parseConfig(content) is
Ok(c) -> Task.ok!(c)
Err(ParseError(msg)) ->
Stdout.line!("Config error: \(msg)")
Task.err!(ConfigError)
# Use config
Stdout.line!("Starting server on \(config.host):\(Num.toStr(config.port))")
After (Haskell):
{-# LANGUAGE DeriveGeneric #-}
import qualified Data.Text.IO as TIO
import qualified Data.Text as T
import Data.Aeson (FromJSON, eitherDecode)
import GHC.Generics
import Control.Monad.Except
import qualified Data.ByteString.Lazy as BL
data Config = Config
{ port :: Word16
, host :: Text
} deriving (Generic, Show)
instance FromJSON Config
data AppError
= ParseError String
| ConfigError
deriving (Show, Eq)
parseConfig :: BL.ByteString -> Either AppError Config
parseConfig content =
case eitherDecode content of
Right config -> Right config
Left err -> Left (ParseError err)
main :: IO ()
main = do
result <- runExceptT $ do
-- Read config file
content <- liftIO $ BL.readFile "config.json"
-- Parse config
config <- case parseConfig content of
Right c -> return c
Left (ParseError msg) -> do
liftIO $ putStrLn $ "Config error: " ++ msg
throwError ConfigError
-- Use config
liftIO $ putStrLn $
"Starting server on " ++ T.unpack (host config)
++ ":" ++ show (port config)
case result of
Left err -> putStrLn $ "Error: " ++ show err
Right _ -> return ()
For more examples and patterns, see:
meta-convert-dev - Foundational patterns with cross-language examplesconvert-elm-haskell - Similar pure functional language conversionlang-roc-dev - Roc development patternslang-haskell-dev - Haskell development patternsCross-cutting pattern skills:
patterns-concurrency-dev - Compare Roc Task model with Haskell async/STMpatterns-serialization-dev - JSON handling across languagespatterns-metaprogramming-dev - Template Haskell vs no metaprogramming in Rocxiaohongshu
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