| name | convert-fsharp-haskell |
| description | Convert F# code to idiomatic Haskell. Use when migrating F# projects to Haskell, translating computation expressions to monadic patterns, or refactoring .NET functional code to pure functional Haskell. Extends meta-convert-dev with F#-to-Haskell specific patterns. |
Convert F# to Haskell
Convert F# code to idiomatic Haskell. This skill extends meta-convert-dev with F#-to-Haskell specific type mappings, idiom translations, and tooling for translating from .NET functional-first to pure functional programming.
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: F# discriminated unions → Haskell algebraic data types
- Idiom translations: Computation expressions → do-notation and monads
- Error handling: Result/Option → Either/Maybe with standard monadic patterns
- Async patterns: F# async workflows → IO monad and async library
- Type providers: F# compile-time generation → Haskell Template Haskell or runtime parsing
- Purity: F# impure-by-default → Haskell pure-by-default with explicit IO
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - F# language fundamentals - see
lang-fsharp-dev - Haskell language fundamentals - see
lang-haskell-dev - Reverse conversion (Haskell → F#) - see
convert-haskell-fsharp - .NET interop specifics - focus on pure functional patterns
Quick Reference
| F# | Haskell | Notes |
|---|---|---|
type Person = { Name: string } |
data Person = Person { name :: String } |
Record types |
type Shape = Circle of float | Square of float |
data Shape = Circle Float | Square Float |
Discriminated unions → ADTs |
Option<'T> |
Maybe a |
Direct correspondence |
Result<'T,'E> |
Either e a |
Note: error on left, value on right |
List<'T> / 'T list |
[a] |
Lists |
async { ... } |
do { ... } in IO monad |
Async workflow → IO actions |
let! / do! |
<- in do-notation |
Monadic bind |
|> (pipe forward) |
$ or & (application) |
Function application |
>> (compose forward) |
. (compose backward) |
Function composition (reversed!) |
[<Measure>] type kg |
Phantom types or units library | Units of measure |
When Converting Code
- Analyze source thoroughly before writing target - understand computation expressions and their translations
- Map types first - create type equivalence table for domain models
- Identify purity boundaries - F# allows side effects anywhere, Haskell requires IO monad
- Adopt target idioms - embrace Haskell's type classes (Functor, Applicative, Monad)
- Handle edge cases - null safety, lazy evaluation, bottom values
- Test equivalence - same inputs → same outputs
- Leverage type system - use Haskell's type classes to reduce boilerplate
Type System Mapping
Primitive Types
| F# | Haskell | Notes |
|---|---|---|
string |
String or Text |
Use Text for performance, String for compatibility |
int |
Int (machine int) or Integer (arbitrary precision) |
F# int is Int32, Haskell Int is machine-word |
int64 |
Int64 or Integer |
Explicit 64-bit |
float / double |
Double |
IEEE 754 double precision |
decimal |
Rational or Scientific |
Exact decimal arithmetic |
bool |
Bool |
Direct mapping |
char |
Char |
Direct mapping |
unit |
() |
Unit type |
byte |
Word8 |
Unsigned 8-bit |
Collection Types
| F# | Haskell | Notes |
|---|---|---|
'T list |
[a] |
Linked lists (similar performance) |
'T array |
Array a (Data.Array) or Vector a |
Immutable arrays; Vector for performance |
List<'T> (.NET) |
[a] or Seq a |
.NET List → Haskell list or sequence |
Set<'T> |
Set a (Data.Set) |
Ordered sets |
Map<'K,'V> |
Map k v (Data.Map) |
Ordered maps |
'T seq |
[a] (lazy evaluation by default) |
Sequences → lazy lists |
('A * 'B) |
(a, b) |
Tuples (direct mapping) |
('A * 'B * 'C) |
(a, b, c) |
Multi-element tuples |
Composite Types
| F# | Haskell | Notes |
|---|---|---|
type alias Person = { ... } |
data Person = Person { ... } |
Records |
type Shape = Circle | Square |
data Shape = Circle Float | Square Float |
Discriminated unions → ADTs |
Option<'T> |
Maybe a |
Some x → Just x, None → Nothing |
Result<'T,'E> |
Either e a |
Ok x → Right x, Error e → Left e |
Async<'T> |
IO a |
F# async → IO monad or async library |
| Single-case union | newtype |
Type safety wrappers |
Idiom Translation
Pattern: Discriminated Unions to Algebraic Data Types
F# and Haskell both have excellent sum type support, but syntax differs.
F#:
type PaymentMethod =
| Cash
| CreditCard of cardNumber: string
| DebitCard of cardNumber: string * pin: int
let processPayment method =
match method with
| Cash -> "Processing cash"
| CreditCard cardNum -> $"Credit card {cardNum}"
| DebitCard (cardNum, _) -> $"Debit card {cardNum}"
// Using the type
let payment = CreditCard "1234-5678"
processPayment payment
Haskell:
data PaymentMethod
= Cash
| CreditCard { cardNumber :: String }
| DebitCard { cardNumber :: String, pin :: Int }
deriving (Show, Eq)
processPayment :: PaymentMethod -> String
processPayment Cash = "Processing cash"
processPayment (CreditCard cardNum) = "Credit card " ++ cardNum
processPayment (DebitCard cardNum _) = "Debit card " ++ cardNum
-- Using the type
payment :: PaymentMethod
payment = CreditCard "1234-5678"
result = processPayment payment
Why this translation:
- Both languages have native sum types with similar semantics
- F#'s
ofkeyword → Haskell's constructor arguments - Named fields in F# → record syntax in Haskell (optional but idiomatic)
- Pattern matching is nearly identical in both languages
Pattern: Records
Both languages have records, but F# updates are syntactically different.
F#:
type Person = {
FirstName: string
LastName: string
Age: int
}
let person = {
FirstName = "Alice"
LastName = "Smith"
Age = 30
}
// Copy-and-update
let olderPerson = { person with Age = 31 }
// Pattern matching
let getFullName { FirstName = f; LastName = l } = $"{f} {l}"
Haskell:
data Person = Person
{ firstName :: String
, lastName :: String
, age :: Int
} deriving (Show, Eq)
person :: Person
person = Person
{ firstName = "Alice"
, lastName = "Smith"
, age = 30
}
-- Record update syntax
olderPerson :: Person
olderPerson = person { age = 31 }
-- Pattern matching with record syntax
getFullName :: Person -> String
getFullName Person{ firstName = f, lastName = l } = f ++ " " ++ l
-- Or with field accessors
getFullName' :: Person -> String
getFullName' p = firstName p ++ " " ++ lastName p
Why this translation:
- Both use similar record update syntax
- Haskell's record syntax includes type constructor in pattern match
- Haskell field accessors are automatic functions (firstName :: Person -> String)
- Both are immutable by default
Pattern: Option/Result to Maybe/Either
F#'s Option and Result types map directly to Haskell's Maybe and Either.
F#:
// Option
let findUser id =
if id = 1 then Some { Name = "Alice"; Age = 30 }
else None
// Using Option
match findUser 1 with
| Some user -> user.Name
| None -> "Unknown"
// Option module functions
let result =
findUser 1
|> Option.map (fun u -> u.Name)
|> Option.defaultValue "Unknown"
// Result
let divide x y =
if y = 0 then Error "Division by zero"
else Ok (x / y)
// Chaining Results
let calculate =
result {
let! a = divide 10 2
let! b = divide 20 4
return a + b
}
Haskell:
-- Maybe
findUser :: Int -> Maybe Person
findUser 1 = Just Person { name = "Alice", age = 30 }
findUser _ = Nothing
-- Using Maybe
case findUser 1 of
Just user -> name user
Nothing -> "Unknown"
-- Maybe functions
result :: String
result = maybe "Unknown" name (findUser 1)
-- Or with fmap and fromMaybe
result' :: String
result' = fromMaybe "Unknown" (name <$> findUser 1)
-- Either
divide :: Double -> Double -> Either String Double
divide _ 0 = Left "Division by zero"
divide x y = Right (x / y)
-- Chaining Either (with do-notation)
calculate :: Either String Double
calculate = do
a <- divide 10 2
b <- divide 20 4
return (a + b)
Why this translation:
- Option → Maybe:
SomebecomesJust,NonebecomesNothing - Result → Either:
OkbecomesRight,ErrorbecomesLeft - F# computation expressions → Haskell do-notation
- Both support similar functional combinators (map, bind, etc.)
Pattern: Computation Expressions to Do-Notation
F#'s computation expressions translate to Haskell's do-notation.
F#:
// Option workflow
type OptionBuilder() =
member _.Bind(x, f) = Option.bind f x
member _.Return(x) = Some x
let option = OptionBuilder()
let validateAge age =
option {
let! valid =
if age >= 0 && age <= 120 then Some age
else None
return valid * 2
}
// Async workflow
let fetchData url = async {
printfn $"Fetching {url}..."
do! Async.Sleep 1000
return $"Data from {url}"
}
let processUrls urls = async {
let! results =
urls
|> List.map fetchData
|> Async.Parallel
return results |> Array.toList
}
Haskell:
-- Maybe monad (built-in, no need for custom builder)
validateAge :: Int -> Maybe Int
validateAge age = do
valid <- if age >= 0 && age <= 120
then Just age
else Nothing
return (valid * 2)
-- IO monad for async operations
fetchData :: String -> IO String
fetchData url = do
putStrLn $ "Fetching " ++ url ++ "..."
threadDelay 1000000 -- 1 second in microseconds
return $ "Data from " ++ url
-- Using Control.Concurrent.Async for parallel
import Control.Concurrent.Async (mapConcurrently)
processUrls :: [String] -> IO [String]
processUrls urls = mapConcurrently fetchData urls
Why this translation:
- F# computation expressions ≈ Haskell monads
let!in F# ≈<-in Haskell do-notationreturnworks the same in both- F# custom builders not needed in Haskell (monads are first-class)
- F#
Async.Parallel→ HaskellmapConcurrentlyfrom async library
Pattern: Pipeline Operators
F# pipelines translate to Haskell application and composition.
F#:
// Forward pipe |>
let result =
[1..10]
|> List.filter (fun x -> x % 2 = 0)
|> List.map (fun x -> x * x)
|> List.sum
// Forward composition >>
let processData = List.filter even >> List.map ((*) 2) >> List.sum
result = processData [1..10]
// Backward pipe <|
let result = List.sum <| List.map ((*) 2) <| List.filter even [1..10]
Haskell:
-- Use $ for application (like <|)
result :: Int
result =
sum $ map (^2) $ filter even [1..10]
-- Or use . for composition (backward, like <<)
processData :: [Int] -> Int
processData = sum . map (^2) . filter even
result' = processData [1..10]
-- For left-to-right, use & from Data.Function
import Data.Function ((&))
result'' :: Int
result'' =
[1..10]
& filter even
& map (^2)
& sum
Why this translation:
- F#
|>(forward) ↔ Haskell$(application) or&(reverse application) - F#
>>(forward composition) ↔ Haskell.(backward composition) - Haskell composition is right-to-left by default (like F#'s
<<) - For F#-style left-to-right, use
&operator
Pattern: Active Patterns to View Patterns / Pattern Synonyms
F# active patterns have no direct equivalent, but Haskell offers alternatives.
F#:
// Single-case active pattern
let (|Even|Odd|) n =
if n % 2 = 0 then Even else Odd
match 42 with
| Even -> "even"
| Odd -> "odd"
// Partial active pattern
let (|Integer|_|) (str: string) =
match System.Int32.TryParse(str) with
| true, value -> Some value
| false, _ -> None
match "123" with
| Integer n -> $"Number: {n}"
| _ -> "Not a number"
Haskell:
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ViewPatterns #-}
-- View patterns approach
isEven :: Int -> Maybe ()
isEven n | even n = Just ()
| otherwise = Nothing
isOdd :: Int -> Maybe ()
isOdd n | odd n = Just ()
| otherwise = Nothing
-- Usage with view patterns
describe :: Int -> String
describe (isEven -> Just ()) = "even"
describe (isOdd -> Just ()) = "odd"
-- Pattern synonyms (simpler for this case)
pattern Even :: Int
pattern Even <- (even -> True)
pattern Odd :: Int
pattern Odd <- (odd -> True)
describe' :: Int -> String
describe' Even = "even"
describe' Odd = "odd"
-- Parsing integers
parseInteger :: String -> Maybe Int
parseInteger = readMaybe
describe'' :: String -> String
describe'' (parseInteger -> Just n) = "Number: " ++ show n
describe'' _ = "Not a number"
Why this translation:
- F# active patterns → Haskell view patterns or pattern synonyms
- View patterns more verbose but more powerful
- Pattern synonyms simpler for basic cases
readMaybefrom Text.Read provides safe parsing
Pattern: Type Providers to Runtime Parsing
F# type providers provide compile-time types from data. Haskell typically uses runtime parsing or Template Haskell.
F#:
open FSharp.Data
// CSV Type Provider (compile-time)
type StockData = CsvProvider<"stocks.csv">
let data = StockData.Load("stocks.csv")
for row in data.Rows do
printfn $"{row.Date}: {row.Close}"
// JSON Type Provider
type Weather = JsonProvider<"""
{
"temperature": 72,
"condition": "sunny"
}
""">
let weather = Weather.Load("weather.json")
printfn $"Temperature: {weather.Temperature}°F"
Haskell:
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
import GHC.Generics
import qualified Data.ByteString.Lazy as B
-- Define types explicitly
data StockRow = StockRow
{ date :: String
, close :: Double
} deriving (Generic, Show)
instance FromJSON StockRow
-- Load and parse at runtime
loadStocks :: FilePath -> IO [StockRow]
loadStocks path = do
contents <- B.readFile path
case decode contents of
Just rows -> return rows
Nothing -> error "Failed to parse CSV"
-- JSON
data Weather = Weather
{ temperature :: Int
, condition :: String
} deriving (Generic, Show)
instance FromJSON Weather
loadWeather :: FilePath -> IO Weather
loadWeather path = do
contents <- B.readFile path
case decode contents of
Just w -> return w
Nothing -> error "Failed to parse JSON"
-- Alternative: Use Template Haskell for compile-time generation
{-# LANGUAGE TemplateHaskell #-}
import Data.Aeson.TH
$(deriveJSON defaultOptions ''Weather) -- Generate instances
Why this translation:
- F# type providers are compile-time, Haskell alternatives are runtime or TH
- Haskell's
aesonlibrary provides safe JSON parsing with types - Template Haskell can generate instances but not infer from samples
- Trade-off: Less magic, more explicit types, same type safety at use site
Error Handling
F# Result/Option → Haskell Either/Maybe
F# and Haskell have similar approaches to explicit error handling.
Comparison:
| Aspect | F# | Haskell |
|---|---|---|
| Optional values | Option<'T> |
Maybe a |
| Result type | Result<'T,'E> |
Either e a |
| Success constructor | Ok, Some |
Right, Just |
| Failure constructor | Error, None |
Left, Nothing |
| Chaining | computation expressions | do-notation |
| Standard library | Option, Result modules |
Data.Maybe, Data.Either |
F#:
type ValidationError = string
let validateAge age =
if age >= 0 && age <= 120 then
Ok age
else
Error "Invalid age"
let validateEmail email =
if email.Contains("@") then
Ok email
else
Error "Invalid email"
// Chain validations
let validatePerson age email =
result {
let! validAge = validateAge age
let! validEmail = validateEmail email
return (validAge, validEmail)
}
// Error: "Invalid age"
validatePerson (-1) "test@example.com"
// Ok: (30, "test@example.com")
validatePerson 30 "test@example.com"
Haskell:
type ValidationError = String
validateAge :: Int -> Either ValidationError Int
validateAge age
| age >= 0 && age <= 120 = Right age
| otherwise = Left "Invalid age"
validateEmail :: String -> Either ValidationError String
validateEmail email
| '@' `elem` email = Right email
| otherwise = Left "Invalid email"
-- Chain validations
validatePerson :: Int -> String -> Either ValidationError (Int, String)
validatePerson age email = do
validAge <- validateAge age
validEmail <- validateEmail email
return (validAge, validEmail)
-- Left "Invalid age"
result1 = validatePerson (-1) "test@example.com"
-- Right (30, "test@example.com")
result2 = validatePerson 30 "test@example.com"
Why this translation:
- F#
Result<'T,'E>maps directly toEither e a - Note: Either puts error on left, value on right (opposite intuition)
- Both support monadic composition (computation expressions / do-notation)
- Both fail-fast: first error stops the chain
Concurrency Patterns
F# Async Workflows → Haskell IO and Async Library
F# async workflows are similar to Haskell's IO monad combined with the async library.
Comparison:
| Aspect | F# | Haskell |
|---|---|---|
| Async type | Async<'T> |
IO a (or Async a with library) |
| Running | Async.RunSynchronously |
No separate step (IO runs at main) |
| Concurrency | Async.Parallel, Async.Start |
forkIO, mapConcurrently, race |
| Delay | Async.Sleep |
threadDelay |
| Bind syntax | let! / do! |
<- in do-notation |
F#:
// Simple async
let fetchUser userId = async {
printfn $"Fetching user {userId}"
do! Async.Sleep 1000
return { Id = userId; Name = "Alice" }
}
// Run async
let user = fetchUser 1 |> Async.RunSynchronously
// Parallel execution
let fetchMultiple userIds = async {
let! users =
userIds
|> List.map fetchUser
|> Async.Parallel
return users |> Array.toList
}
// Fire-and-forget
Async.Start(fetchUser 1 |> Async.Ignore)
Haskell:
import Control.Concurrent (threadDelay, forkIO)
import Control.Concurrent.Async
-- IO action (like F# async)
fetchUser :: Int -> IO User
fetchUser userId = do
putStrLn $ "Fetching user " ++ show userId
threadDelay 1000000 -- 1 second (microseconds)
return $ User userId "Alice"
-- Run IO (happens automatically in main)
main :: IO ()
main = do
user <- fetchUser 1
print user
-- Parallel execution with async library
fetchMultiple :: [Int] -> IO [User]
fetchMultiple userIds = mapConcurrently fetchUser userIds
-- Alternative: manual forking
fetchMultiple' :: [Int] -> IO [User]
fetchMultiple' userIds = do
asyncs <- mapM (async . fetchUser) userIds
mapM wait asyncs
-- Fire-and-forget
main' :: IO ()
main' = do
forkIO $ fetchUser 1 >> return ()
-- Continue with other work
return ()
Why this translation:
- F#
Async<'T>conceptually similar toIO a - F# async needs explicit
RunSynchronously; Haskell IO runs in main Async.Parallel→mapConcurrentlyfrom async librarythreadDelaytakes microseconds (1000000 = 1 second)- Haskell's async library provides structured concurrency
Memory & Ownership
F# (GC + Mutable) → Haskell (GC + Immutable)
Both languages are garbage-collected, but Haskell enforces immutability more strictly.
Comparison:
| Aspect | F# | Haskell |
|---|---|---|
| Memory model | GC (via .NET CLR) | GC (via GHC runtime) |
| Mutability | mutable keyword for fields |
Immutable by default, IORef/STM for state |
| References | ref cells |
IORef, TVar, MVar |
| Arrays | Mutable arrays available | Array (immutable), IOArray/STArray (mutable in IO/ST) |
| Laziness | Eager by default | Lazy by default |
F#:
// Mutable field
type Counter() =
let mutable count = 0
member _.Increment() = count <- count + 1
member _.Count = count
// Reference cell
let counter = ref 0
counter := !counter + 1
printfn $"Count: {!counter}"
// Mutable array (in-place updates)
let arr = [| 1; 2; 3 |]
arr.[0] <- 10
Haskell:
import Data.IORef
import Control.Monad (replicateM_)
-- Counter with IORef (mutable in IO)
data Counter = Counter { counterRef :: IORef Int }
newCounter :: IO Counter
newCounter = Counter <$> newIORef 0
increment :: Counter -> IO ()
increment (Counter ref) = modifyIORef' ref (+1)
getCount :: Counter -> IO Int
getCount (Counter ref) = readIORef ref
-- Usage
main :: IO ()
main = do
counter <- newCounter
replicateM_ 5 (increment counter)
count <- getCount counter
putStrLn $ "Count: " ++ show count
-- STRef for local mutation (pure interface)
import Control.Monad.ST
import Data.STRef
localMutation :: Int -> Int
localMutation n = runST $ do
ref <- newSTRef 0
replicateM_ n $ modifySTRef' ref (+1)
readSTRef ref
Why this translation:
- F#
mutable/ref→ HaskellIORef(requires IO monad) - For pure local mutation, use
STRefwithrunST - Haskell's default immutability encourages functional patterns
- Most code doesn't need mutable references
Common Pitfalls
1. Confusing Either Direction
Problem: F#'s Result has Ok/Error, Haskell's Either has Left/Right
-- WRONG: Thinking Right is error
validate x = if x > 0 then Left x else Right "Error"
-- CORRECT: Right is success, Left is error
validate x = if x > 0 then Right x else Left "Error"
Fix: Remember: "Right is right (correct)"
2. Missing IO for Side Effects
Problem: F# allows side effects anywhere, Haskell requires IO
-- WRONG: Can't use putStrLn in pure function
greet :: String -> String
greet name = putStrLn $ "Hello, " ++ name -- Type error!
-- CORRECT: Return IO action
greet :: String -> IO ()
greet name = putStrLn $ "Hello, " ++ name
Fix: Mark functions with side effects as returning IO
3. Composition Direction
Problem: F# >> is forward, Haskell . is backward
-- F# style (forward)
-- let process = add1 >> double >> toString
-- WRONG direct translation
process = add1 . double . toString -- Composes backward!
-- CORRECT: Reverse order
process = toString . double . add1
-- OR: Use <<< for forward composition (less idiomatic)
-- OR: Use & for application
Fix: Reverse composition order or use & for left-to-right
4. Lazy Evaluation Surprises
Problem: Haskell is lazy by default, F# is eager
-- F# would evaluate immediately
-- Haskell delays until needed
result = expensiveComputation 1000000
-- May cause space leaks
badSum = foldl (+) 0 [1..1000000] -- Builds up thunks
-- CORRECT: Use strict operations when needed
goodSum = foldl' (+) 0 [1..1000000] -- Strict fold
Fix: Use strict versions (foldl', seq, ! bang patterns) when needed
5. String Types Confusion
Problem: Haskell has multiple string types
-- String (lazy, inefficient)
name :: String
name = "Alice"
-- Text (strict, efficient)
name' :: Text
name' = "Alice" -- Needs OverloadedStrings
-- ByteString (for binary data)
data' :: ByteString
data' = "binary"
Fix: Use Text for Unicode strings, ByteString for binary, String for simple cases
6. Type Class Constraints Not Explicit
Problem: F# inference works differently than Haskell's
-- F# infers numeric type
-- F# let add x y = x + y (generic numeric)
-- Haskell needs constraint
add :: Num a => a -> a -> a
add x y = x + y
Fix: Add type signatures with constraints
Tooling
| Tool | F# | Haskell | Notes |
|---|---|---|---|
| Build | dotnet build |
cabal build / stack build |
Stack is more batteries-included |
| REPL | dotnet fsi |
ghci / stack ghci |
Interactive development |
| Package Manager | NuGet (via dotnet) | Cabal / Hackage | Stack uses Stackage curated sets |
| Formatter | Fantomas | ormolu / fourmolu / stylish-haskell |
Multiple choices in Haskell |
| Linter | FSharpLint | hlint |
Suggests improvements |
| Type Checker | F# compiler | GHC | GHC has more advanced type features |
| Testing | Expecto, xUnit, FsUnit | HSpec, QuickCheck, Hedgehog, Tasty | Property testing popular in Haskell |
| Language Server | Ionide | HLS (Haskell Language Server) | IDE integration |
Examples
Example 1: Simple - Record Type Conversion
Convert a simple F# record to Haskell.
Before (F#):
type User = {
Id: int
Name: string
Email: string
Age: int
}
let createUser id name email age = {
Id = id
Name = name
Email = email
Age = age
}
let getEmail user = user.Email
let updateAge age user = { user with Age = age }
// Usage
let user = createUser 1 "Alice" "alice@example.com" 30
let email = getEmail user
let older = updateAge 31 user
After (Haskell):
data User = User
{ userId :: Int
, userName :: String
, userEmail :: String
, userAge :: Int
} deriving (Show, Eq)
createUser :: Int -> String -> String -> Int -> User
createUser id' name email age = User
{ userId = id'
, userName = name
, userEmail = email
, userAge = age
}
getEmail :: User -> String
getEmail = userEmail -- Record accessor is a function
updateAge :: Int -> User -> User
updateAge age user = user { userAge = age }
-- Usage
user :: User
user = createUser 1 "Alice" "alice@example.com" 30
email :: String
email = getEmail user
older :: User
older = updateAge 31 user
Example 2: Medium - Discriminated Unions and Pattern Matching
Convert F# discriminated unions and pattern matching.
Before (F#):
type Expression =
| Literal of int
| Add of Expression * Expression
| Multiply of Expression * Expression
| Variable of string
let rec evaluate env expr =
match expr with
| Literal n -> n
| Add (left, right) ->
evaluate env left + evaluate env right
| Multiply (left, right) ->
evaluate env left * evaluate env right
| Variable name ->
Map.find name env
let simplify expr =
match expr with
| Add (Literal 0, e) -> e
| Add (e, Literal 0) -> e
| Multiply (Literal 1, e) -> e
| Multiply (e, Literal 1) -> e
| Multiply (Literal 0, _) -> Literal 0
| Multiply (_, Literal 0) -> Literal 0
| e -> e
// Usage
let env = Map [("x", 5); ("y", 10)]
let expr = Add (Variable "x", Multiply (Literal 2, Variable "y"))
let result = evaluate env expr // 5 + (2 * 10) = 25
After (Haskell):
import qualified Data.Map as Map
data Expression
= Literal Int
| Add Expression Expression
| Multiply Expression Expression
| Variable String
deriving (Show, Eq)
type Environment = Map.Map String Int
evaluate :: Environment -> Expression -> Int
evaluate env (Literal n) = n
evaluate env (Add left right) =
evaluate env left + evaluate env right
evaluate env (Multiply left right) =
evaluate env left * evaluate env right
evaluate env (Variable name) =
env Map.! name -- Partial function, use Map.lookup for safety
simplify :: Expression -> Expression
simplify (Add (Literal 0) e) = e
simplify (Add e (Literal 0)) = e
simplify (Multiply (Literal 1) e) = e
simplify (Multiply e (Literal 1)) = e
simplify (Multiply (Literal 0) _) = Literal 0
simplify (Multiply _ (Literal 0)) = Literal 0
simplify e = e
-- Usage
env :: Environment
env = Map.fromList [("x", 5), ("y", 10)]
expr :: Expression
expr = Add (Variable "x") (Multiply (Literal 2) (Variable "y"))
result :: Int
result = evaluate env expr -- 5 + (2 * 10) = 25
Example 3: Complex - Async Workflows to IO with Concurrency
Convert F# async workflows to Haskell IO with the async library.
Before (F#):
open System
type User = { Id: int; Name: string; Email: string }
type ApiError = NetworkError of string | ParseError of string
let fetchFromApi url = async {
printfn $"Fetching from {url}"
do! Async.Sleep 500
if url.Contains("fail") then
return Error (NetworkError "Network failed")
else
return Ok $"Data from {url}"
}
let parseUser data =
if data.Contains("error") then
Error (ParseError "Parse failed")
else
Ok { Id = 1; Name = "Alice"; Email = "alice@example.com" }
let getUserData url = async {
let! fetchResult = fetchFromApi url
return
fetchResult
|> Result.bind parseUser
}
let getAllUsers urls = async {
let! results =
urls
|> List.map getUserData
|> Async.Parallel
return results |> Array.toList
}
// Usage
let urls = ["https://api.example.com/user/1"; "https://api.example.com/user/2"]
let results = getAllUsers urls |> Async.RunSynchronously
After (Haskell):
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (mapConcurrently)
import Data.List (isInfixOf)
data User = User
{ userId :: Int
, userName :: String
, userEmail :: String
} deriving (Show, Eq)
data ApiError
= NetworkError String
| ParseError String
deriving (Show, Eq)
fetchFromApi :: String -> IO (Either ApiError String)
fetchFromApi url = do
putStrLn $ "Fetching from " ++ url
threadDelay 500000 -- 500ms in microseconds
return $ if "fail" `isInfixOf` url
then Left (NetworkError "Network failed")
else Right ("Data from " ++ url)
parseUser :: String -> Either ApiError User
parseUser dat
| "error" `isInfixOf` dat = Left (ParseError "Parse failed")
| otherwise = Right $ User 1 "Alice" "alice@example.com"
getUserData :: String -> IO (Either ApiError User)
getUserData url = do
fetchResult <- fetchFromApi url
return $ fetchResult >>= parseUser -- Chaining Either with >>=
getAllUsers :: [String] -> IO [Either ApiError User]
getAllUsers urls = mapConcurrently getUserData urls
-- Usage
main :: IO ()
main = do
let urls = ["https://api.example.com/user/1", "https://api.example.com/user/2"]
results <- getAllUsers urls
print results
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language exampleslang-fsharp-dev- F# development patternslang-haskell-dev- Haskell development patterns
Cross-cutting pattern skills:
patterns-concurrency-dev- Async, STM, threading across languagespatterns-serialization-dev- JSON, validation across languagespatterns-metaprogramming-dev- Type providers vs Template Haskell