| name | convert-haskell-fsharp |
| description | Convert Haskell code to idiomatic F#. Use when migrating Haskell projects to F#, translating Haskell patterns to idiomatic F#, or refactoring Haskell codebases. Extends meta-convert-dev with Haskell-to-F# specific patterns. |
Convert Haskell to F#
Convert Haskell code to idiomatic F#. This skill extends meta-convert-dev with Haskell-to-F# specific type mappings, idiom translations, and tooling.
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 types → F# types
- Idiom translations: Haskell patterns → idiomatic F#
- Error handling: Haskell Maybe/Either → F# Option/Result
- Type classes: Haskell type classes → F# interfaces/traits
- Evaluation: Lazy evaluation → eager evaluation with seq
- Ecosystem: GHC/Cabal/Stack → .NET/NuGet/dotnet CLI
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Haskell language fundamentals - see
lang-haskell-dev - F# language fundamentals - see
lang-fsharp-dev - Reverse conversion (F# → Haskell) - see
convert-fsharp-haskell
Quick Reference
| Haskell | F# | Notes |
|---|---|---|
String |
string |
Direct mapping |
Int |
int |
32-bit signed integer |
Integer |
bigint |
Arbitrary precision |
Float/Double |
float |
64-bit floating point |
Bool |
bool |
Direct mapping |
Char |
char |
Direct mapping |
[a] |
list<'a> |
Immutable linked list |
Maybe a |
option<'a> |
Optional values |
Either a b |
Result<'b,'a> |
Note: order swapped |
(a, b) |
'a * 'b |
Tuple (different syntax) |
data X = ... |
type X = ... |
Discriminated union |
type X = ... |
type X = ... |
Type alias |
newtype X = ... |
type X = X of ... |
Single-case union |
class C a where |
type I = interface |
Type class → interface |
IO a |
Async<'a> or Task<'a> |
Side effects |
When Converting Code
- Analyze source thoroughly before writing target
- Map types first - create type equivalence table
- Handle laziness - Haskell is lazy by default, F# is eager
- Adopt F# idioms - don't write "Haskell code in F# syntax"
- Replace type classes - use interfaces or module functions
- Handle edge cases - pattern matching, infinite lists, laziness
- Test equivalence - same inputs → same outputs
Type System Mapping
Primitive Types
| Haskell | F# | Notes |
|---|---|---|
Int |
int |
32-bit signed integer |
Integer |
bigint |
Arbitrary precision (System.Numerics.BigInteger) |
Float |
float32 / single |
32-bit floating point |
Double |
float / double |
64-bit floating point (default) |
Bool |
bool |
Direct mapping |
Char |
char |
Unicode character |
String |
string |
Unicode string |
() |
unit |
Unit type |
Collection Types
| Haskell | F# | Notes |
|---|---|---|
[a] |
'a list |
Immutable linked list |
[a] (infinite) |
seq<'a> |
Use sequences for lazy evaluation |
(a, b) |
'a * 'b |
Tuple (note: * syntax in types) |
(a, b, c) |
'a * 'b * 'c |
Tuple with 3+ elements |
Map k v |
Map<'k,'v> |
Immutable map |
Set a |
Set<'a> |
Immutable set |
Array a |
'a[] or array<'a> |
Mutable array |
Vector a |
'a[] |
Arrays are more common in F# |
Composite Types
| Haskell | F# | Notes |
|---|---|---|
data X = C1 | C2 |
type X = C1 | C2 |
Discriminated union |
data X = C a b |
type X = C of 'a * 'b |
Union with data |
data X = C { f :: a } |
type X = { F: 'a } |
Record type |
newtype X = X a |
type X = X of 'a |
Single-case discriminated union |
type X = a |
type X = 'a |
Type alias |
Option and Result Types
| Haskell | F# | Notes |
|---|---|---|
Maybe a |
'a option |
Some/None instead of Just/Nothing |
Just x |
Some x |
Present value |
Nothing |
None |
Absent value |
Either a b |
Result<'b,'a> |
Note: parameter order is swapped! |
Left err |
Error err |
Error case |
Right val |
Ok val |
Success case |
Type Classes → Interfaces/Modules
| Haskell | F# | Strategy |
|---|---|---|
class Eq a where |
interface IEquatable<'a> or = operator |
Built-in equality |
class Ord a where |
interface IComparable<'a> or compare |
Built-in comparison |
class Show a where |
Override ToString() or use sprintf |
String representation |
class Functor f where |
Module with map function |
No direct equivalent |
class Applicative f where |
Module with combinators | No direct equivalent |
class Monad m where |
Computation expressions | Use let! and return |
Idiom Translation
Pattern 1: Maybe/Either → Option/Result
Haskell:
findUser :: String -> Maybe User
findUser userId = case lookup userId users of
Just user -> Just user
Nothing -> Nothing
validateAge :: Int -> Either String Int
validateAge age
| age < 0 = Left "Age cannot be negative"
| age > 150 = Left "Age too high"
| otherwise = Right age
F#:
let findUser (userId: string) : User option =
users |> Map.tryFind userId
let validateAge (age: int) : Result<int, string> =
if age < 0 then
Error "Age cannot be negative"
elif age > 150 then
Error "Age too high"
else
Ok age
Why this translation:
- F#'s
Optionmaps directly to Haskell'sMaybe - F#'s
Result<'T,'Error>maps to Haskell'sEither, but parameter order is swapped - F# uses
Some/Noneinstead ofJust/Nothing - F# uses
Ok/Errorinstead ofRight/Left - Pattern matching syntax is similar but uses
match ... with
Pattern 2: List Comprehensions
Haskell:
squares :: [Int]
squares = [x^2 | x <- [1..10]]
pythagoras :: [(Int, Int, Int)]
pythagoras = [(a,b,c) | a <- [1..20],
b <- [a..20],
c <- [b..20],
a^2 + b^2 == c^2]
F#:
let squares: int list =
[ for x in 1..10 -> x * x ]
let pythagoras: (int * int * int) list =
[ for a in 1..20 do
for b in a..20 do
for c in b..20 do
if a*a + b*b = c*c then
yield (a, b, c) ]
Why this translation:
- F# uses
[ for x in ... ]instead of Haskell's[ x | ... ] - F# uses
->for simple projections,yieldfor conditional yields - F# requires
dofor multiple generators - F# uses
=for equality instead of==
Pattern 3: Higher-Order Functions
Haskell:
result :: [Int] -> Int
result = sum . map (*2) . filter even
addThenDouble :: Int -> Int
addThenDouble = (*2) . (+1)
F#:
let result (xs: int list) : int =
xs
|> List.filter (fun x -> x % 2 = 0)
|> List.map (fun x -> x * 2)
|> List.sum
let addThenDouble: int -> int =
(+) 1 >> (*) 2
Why this translation:
- F# uses
|>(pipe forward) instead of.(compose) - F# reads left-to-right with
|>, Haskell reads right-to-left with. - F# uses
>>for function composition (same direction as Haskell's.) - F# requires explicit lambda syntax
fun x -> ...more often
Pattern 4: Pattern Matching and Guards
Haskell:
classify :: Int -> String
classify n
| n < 0 = "negative"
| n == 0 = "zero"
| n < 10 = "small"
| otherwise = "large"
describeList :: [a] -> String
describeList [] = "empty"
describeList [x] = "singleton"
describeList xs = "longer"
F#:
let classify (n: int) : string =
match n with
| n when n < 0 -> "negative"
| 0 -> "zero"
| n when n < 10 -> "small"
| _ -> "large"
let describeList (xs: 'a list) : string =
match xs with
| [] -> "empty"
| [x] -> "singleton"
| _ -> "longer"
Why this translation:
- Haskell uses guards (
|) in function definitions - F# uses
match ... withexpression withwhenguards - F# can match literals directly without guards
- Both support pattern matching on list structure
Pattern 5: Recursive Functions
Haskell:
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)
-- Tail-recursive
factorial' :: Integer -> Integer
factorial' n = go n 1
where
go 0 acc = acc
go n acc = go (n - 1) (acc * n)
F#:
let rec factorial (n: bigint) : bigint =
if n = 0I then 1I
else n * factorial (n - 1I)
// Tail-recursive
let factorial' (n: bigint) : bigint =
let rec loop n acc =
if n = 0I then acc
else loop (n - 1I) (acc * n)
loop n 1I
Why this translation:
- F# requires explicit
reckeyword for recursive functions - F# uses
let ... infor local definitions instead ofwhere - F# can use pattern matching in function parameters, but if/match is more common
- Tail recursion optimization works similarly in both languages
Pattern 6: Typeclasses → Interfaces/Modules
Haskell:
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
data Color = Red | Green | Blue
instance Eq Color where
Red == Red = True
Green == Green = True
Blue == Blue = True
_ == _ = False
x /= y = not (x == y)
F#:
type Color =
| Red
| Green
| Blue
// F# generates equality automatically for discriminated unions
// Manual implementation:
type Color with
static member op_Equality(a: Color, b: Color) =
match a, b with
| Red, Red -> true
| Green, Green -> true
| Blue, Blue -> true
| _ -> false
// Or use structural equality (automatic for DUs)
let color1 = Red
let color2 = Red
let isEqual = (color1 = color2) // true
Why this translation:
- F# doesn't have type classes
- F# discriminated unions have automatic structural equality
- Custom equality requires overriding
Equalsor operator overloading - For polymorphic behavior, use interfaces or module functions
Pattern 7: Functors and Monads → Computation Expressions
Haskell:
-- Functor usage
result = fmap (+1) (Just 5) -- Just 6
-- Monad usage (do-notation)
process :: Maybe Int
process = do
x <- Just 10
y <- Just 20
return (x + y)
-- Either monad
validate :: Either String Int
validate = do
age <- validateAge 25
score <- validateScore 85
return (age + score)
F#:
// Functor-like usage
let result = Option.map (fun x -> x + 1) (Some 5) // Some 6
// Computation expression (option)
let process: int option =
option {
let! x = Some 10
let! y = Some 20
return x + y
}
// Result computation expression
let validate: Result<int, string> =
result {
let! age = validateAge 25
let! score = validateScore 85
return age + score
}
Why this translation:
- F# computation expressions are similar to Haskell's do-notation
let!in F# ≈<-in Haskellreturnworks the same way- F# requires explicit computation expression builders (
option,result,async) - Some builders are built-in, others need to be defined or imported
Pattern 8: Lazy Evaluation → Sequences
Haskell:
-- Infinite list (lazy by default)
naturals :: [Integer]
naturals = [1..]
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
takeFirst10 :: [Integer]
takeFirst10 = take 10 fibs
F#:
// Lazy sequence (must be explicit)
let naturals: seq<bigint> =
Seq.initInfinite (fun i -> bigint i + 1I)
let fibs: seq<bigint> =
(0I, 1I)
|> Seq.unfold (fun (a, b) -> Some(a, (b, a + b)))
let takeFirst10: bigint list =
fibs |> Seq.take 10 |> Seq.toList
Why this translation:
- Haskell lists are lazy by default
- F# lists are eager, sequences (
seq<'a>) are lazy - F#
Seq.unfoldreplaces recursive definitions - Must explicitly convert sequences to lists with
Seq.toList - F# doesn't support infinite lists natively, use sequences
Pattern 9: Type Families → Generic Types
Haskell:
class Container c where
type Elem c :: *
empty :: c
insert :: Elem c -> c -> c
instance Container [a] where
type Elem [a] = a
empty = []
insert = (:)
F#:
type IContainer<'c, 'elem> =
abstract member Empty: 'c
abstract member Insert: 'elem -> 'c -> 'c
type ListContainer<'a>() =
interface IContainer<'a list, 'a> with
member _.Empty = []
member _.Insert elem container = elem :: container
Why this translation:
- Haskell type families → F# generic interfaces
- No direct equivalent to associated types
- Use explicit type parameters
- Requires more verbose interface definitions
Pattern 10: Module System
Haskell:
-- MyApp/User.hs
module MyApp.User
( User(..)
, createUser
, validateEmail
) where
import Data.Text (Text)
import qualified Data.Map as M
data User = User
{ name :: Text
, email :: Text
}
createUser :: Text -> Text -> User
createUser n e = User n e
F#:
// User.fs
module MyApp.User
type User = {
Name: string
Email: string
}
let createUser (name: string) (email: string) : User =
{ Name = name; Email = email }
let validateEmail (email: string) : bool =
email.Contains("@")
Why this translation:
- F# modules are file-based (one module per file by default)
- F# uses
module Nameat top of file - No explicit export list in F#; everything is public by default
- Use
internalorprivatefor visibility control - F# records use
{ Field: Type }syntax instead of Haskell's{ field :: Type }
Error Handling
Haskell Maybe/Either → F# Option/Result
Haskell uses Maybe for optional values and Either for computations that can fail. F# uses Option and Result respectively.
| Haskell | F# | Notes |
|---|---|---|
Maybe a |
'a option |
Optional values |
Just x |
Some x |
Present value |
Nothing |
None |
Absent value |
Either a b |
Result<'b,'a> |
Parameters swapped! |
Left err |
Error err |
Error case |
Right val |
Ok val |
Success case |
fromMaybe |
defaultArg or Option.defaultValue |
Provide default |
maybe |
Option.fold |
Fold over option |
Basic Error Translation
Haskell:
safeDivide :: Double -> Double -> Maybe Double
safeDivide _ 0 = Nothing
safeDivide x y = Just (x / y)
parseAge :: String -> Either String Int
parseAge str =
case reads str of
[(n, "")] -> if n >= 0
then Right n
else Left "Age must be positive"
_ -> Left "Not a valid number"
F#:
let safeDivide (x: float) (y: float) : float option =
if y = 0.0 then None
else Some (x / y)
let parseAge (str: string) : Result<int, string> =
match System.Int32.TryParse(str) with
| true, n when n >= 0 -> Ok n
| true, _ -> Error "Age must be positive"
| false, _ -> Error "Not a valid number"
Chaining Operations
Haskell:
-- Option chaining
getUserEmail :: UserId -> Maybe Email
getUserEmail userId = do
user <- findUser userId
return (email user)
-- Or with bind
getUserEmail' :: UserId -> Maybe Email
getUserEmail' userId =
findUser userId >>= return . email
-- Either chaining
validateUser :: String -> String -> Either String User
validateUser ageStr emailStr = do
age <- parseAge ageStr
email <- validateEmail emailStr
return $ User email age
F#:
// Option chaining with computation expression
let getUserEmail (userId: UserId) : Email option =
option {
let! user = findUser userId
return user.Email
}
// Or with bind
let getUserEmail' (userId: UserId) : Email option =
findUser userId
|> Option.map (fun user -> user.Email)
// Result chaining
let validateUser (ageStr: string) (emailStr: string) : Result<User, string> =
result {
let! age = parseAge ageStr
let! email = validateEmail emailStr
return { Email = email; Age = age }
}
Evaluation Strategy
Lazy vs Eager Evaluation
Haskell is lazy by default, while F# is eager by default. This is one of the most significant differences.
| Haskell | F# | Strategy |
|---|---|---|
| Lazy by default | Eager by default | Use seq for laziness |
[1..] |
Seq.initInfinite |
Infinite sequences |
take 10 [1..] |
Seq.take 10 (Seq.initInfinite id) |
Take from infinite |
let x = expr |
let x = lazy expr |
Explicit lazy values |
x |
x.Force() |
Force lazy evaluation |
Converting Lazy Code
Haskell:
-- Infinite list of fibonacci numbers
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
-- Take first 10
first10 :: [Integer]
first10 = take 10 fibs
-- Infinite list of primes (conceptual)
primes :: [Integer]
primes = sieve [2..]
where
sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
F#:
// Lazy sequence of fibonacci numbers
let fibs: seq<bigint> =
(0I, 1I)
|> Seq.unfold (fun (a, b) ->
Some(a, (b, a + b)))
// Take first 10
let first10: bigint list =
fibs |> Seq.take 10 |> Seq.toList
// Lazy sequence of primes
let primes: seq<bigint> =
let rec sieve (nums: seq<bigint>) : seq<bigint> = seq {
let p = Seq.head nums
yield p
yield! sieve (nums |> Seq.filter (fun x -> x % p <> 0I))
}
sieve (Seq.initInfinite (fun i -> bigint i + 2I))
Explicit Lazy Values
Haskell:
-- Everything is lazy
expensiveComputation :: Int
expensiveComputation = sum [1..1000000]
-- Used like any value
result :: Int
result = if condition then expensiveComputation else 0
F#:
// Must explicitly mark as lazy
let expensiveComputation: Lazy<int> =
lazy (
List.sum [1..1000000]
)
// Must force evaluation
let result: int =
if condition then expensiveComputation.Force()
else 0
Concurrency Patterns
Haskell Concurrency → F# Async
Haskell uses lightweight threads and STM, while F# uses async workflows and Task-based async.
| Haskell | F# | Notes |
|---|---|---|
forkIO |
Async.Start |
Spawn concurrent computation |
MVar |
MailboxProcessor |
Message-passing concurrency |
STM |
No direct equivalent | Use agents or locks |
async library |
async { } |
Async computations |
Async a |
Async<'a> |
Async type |
Basic Async Translation
Haskell:
import Control.Concurrent.Async
fetchData :: String -> IO String
fetchData url = do
threadDelay 1000000
return $ "Data from " ++ url
main :: IO ()
main = do
result1 <- async (fetchData "url1")
result2 <- async (fetchData "url2")
data1 <- wait result1
data2 <- wait result2
putStrLn $ data1 ++ ", " ++ data2
F#:
open System
let fetchData (url: string) : Async<string> = async {
do! Async.Sleep 1000
return $"Data from {url}"
}
[<EntryPoint>]
let main argv =
let result =
async {
let! data1 = fetchData "url1"
let! data2 = fetchData "url2"
return $"{data1}, {data2}"
}
|> Async.RunSynchronously
printfn "%s" result
0
Parallel Execution
Haskell:
import Control.Concurrent.Async
fetchAll :: IO ([User], [Order])
fetchAll = do
(users, orders) <- concurrently fetchUsers fetchOrders
return (users, orders)
F#:
let fetchAll: Async<User list * Order list> = async {
let! users, orders =
Async.Parallel [
fetchUsers() |> Async.map (fun x -> Choice1Of2 x)
fetchOrders() |> Async.map (fun x -> Choice2Of2 x)
]
|> Async.map (fun results ->
// Handle results...
([], []) // Simplified
)
return users, orders
}
// Or simpler with tuple:
let fetchAll': Async<User list * Order list> = async {
let! results =
(fetchUsers(), fetchOrders())
||> Async.Parallel2
return results
}
Common Pitfalls
1. Confusing Result Parameter Order
Problem: Haskell's Either a b has error first, F#'s Result<'T,'E> has success first.
Example:
-- Haskell: Either ErrorType SuccessType
parseConfig :: String -> Either String Config
parseConfig str = Right config -- Success is Right
// F#: Result<SuccessType, ErrorType>
let parseConfig (str: string) : Result<Config, string> =
Ok config // Success is Ok
Solution: Remember F# swaps the order. When converting:
- Haskell
Either e a→ F#Result<a, e> - Haskell
Left err→ F#Error err - Haskell
Right val→ F#Ok val
2. Forgetting Lazy vs Eager Evaluation
Problem: Infinite lists work in Haskell but cause infinite loops in F#.
Example:
-- Haskell: works fine (lazy)
allNumbers = [1..]
firstTen = take 10 allNumbers
// F#: This will hang! (eager)
// let allNumbers = [1..] // Doesn't compile
// Correct: use sequences
let allNumbers = Seq.initInfinite ((+) 1)
let firstTen = allNumbers |> Seq.take 10 |> Seq.toList
Solution: Use seq<'a> for lazy evaluation in F#, not lists.
3. Type Class Constraints Not Translating
Problem: Haskell type class constraints don't have direct F# equivalents.
Example:
-- Haskell: polymorphic with type class
sumAll :: (Num a) => [a] -> a
sumAll = foldr (+) 0
// F#: must be specific or use inline
let sumAll (xs: int list) : int =
List.fold (+) 0 xs
// Or use inline for polymorphism
let inline sumAll xs =
List.fold (+) LanguagePrimitives.GenericZero xs
Solution: Use inline functions or make types concrete.
4. Pattern Matching Syntax Differences
Problem: Haskell allows pattern matching in function definitions, F# requires match.
Example:
-- Haskell: patterns in function definition
factorial 0 = 1
factorial n = n * factorial (n - 1)
// F#: must use match or if
let rec factorial (n: int) : int =
match n with
| 0 -> 1
| n -> n * factorial (n - 1)
// Or with if
let rec factorial' (n: int) : int =
if n = 0 then 1
else n * factorial' (n - 1)
Solution: Use match ... with for pattern matching in F#.
5. Module Import Differences
Problem: Haskell's qualified imports don't translate directly.
Example:
-- Haskell
import qualified Data.Map as M
import Data.List (sort, nub)
result = M.lookup "key" map
// F#: different syntax
open System.Collections.Generic
// For modules, open at top
// No "qualified" keyword
// Use full paths for disambiguation
let result = Map.tryFind "key" map
Solution: F# uses open for imports, use full module paths for disambiguation.
6. Tuple Syntax Confusion
Problem: Tuple constructor vs type syntax differs.
Example:
-- Haskell: consistent syntax
value :: (Int, String)
value = (42, "hello")
// F#: different syntax for type vs value
let value: int * string = // Type uses *
(42, "hello") // Value uses ,
Solution: Remember F# uses * in types, , in values.
7. No Automatic Currying in All Contexts
Problem: F# functions are curried, but some .NET APIs are not.
Example:
-- Haskell: currying everywhere
add x y = x + y
add5 = add 5 -- Partial application
// F#: currying works for F# functions
let add x y = x + y
let add5 = add 5 // Works
// But .NET methods are not curried
// Math.Max(1, 2) // Must provide all args
let max1 = Math.Max(1) // Error!
// Wrap in F# function for currying
let max a b = Math.Max(a, b)
let max1' = max 1 // Works
Solution: Wrap .NET methods in F# functions for partial application.
8. String Type Differences
Problem: Haskell String is [Char], F# string is System.String.
Example:
-- Haskell: String is a list
reverse :: String -> String
reverse = reverse -- List reverse works on strings
head :: String -> Char
head (c:_) = c
F#:
// F#: string is not a list
let reverse (s: string) : string =
s.ToCharArray()
|> Array.rev
|> System.String
let head (s: string) : char =
s.[0] // Index, not pattern matching
Solution: Use string methods and array operations in F#, not list operations.
Tooling
| Category | Haskell | F# Equivalent |
|---|---|---|
| Compiler | GHC | F# compiler (fsc) |
| Build Tool | Cabal, Stack | dotnet CLI |
| Package Manager | Cabal, Stack | NuGet |
| REPL | GHCi | FSI (F# Interactive) |
| Formatter | stylish-haskell, ormolu | fantomas |
| Linter | HLint | FSharpLint |
| IDE | VS Code + HLS, IntelliJ IDEA | VS Code, Visual Studio, Rider |
| Documentation | Haddock | XML docs / FSharp.Formatting |
| Testing | HSpec, QuickCheck | Expecto, FsCheck |
Package Ecosystem Mapping
| Purpose | Haskell | F# |
|---|---|---|
| JSON | aeson | FSharp.Json, System.Text.Json |
| HTTP Client | http-client, req | FSharp.Data, HttpClient |
| Parsing | parsec, megaparsec | FParsec |
| CLI | optparse-applicative | Argu |
| Async | async | Built-in async |
| Testing | QuickCheck | FsCheck |
| Web Framework | Servant, Scotty, Yesod | Giraffe, Saturn, Suave |
Examples
Example 1: Simple - List Processing
Before (Haskell):
module ListUtils where
-- Filter and map in one pass
processNumbers :: [Int] -> [Int]
processNumbers = map (*2) . filter even
-- Find first match
findFirst :: (a -> Bool) -> [a] -> Maybe a
findFirst pred [] = Nothing
findFirst pred (x:xs)
| pred x = Just x
| otherwise = findFirst pred xs
-- Remove duplicates
unique :: Eq a => [a] -> [a]
unique [] = []
unique (x:xs) = x : unique (filter (/= x) xs)
After (F#):
module ListUtils
// Filter and map in one pass
let processNumbers (xs: int list) : int list =
xs
|> List.filter (fun x -> x % 2 = 0)
|> List.map (fun x -> x * 2)
// Find first match
let findFirst (pred: 'a -> bool) (xs: 'a list) : 'a option =
List.tryFind pred xs
// Remove duplicates
let unique (xs: 'a list) : 'a list =
xs |> List.distinct
Example 2: Medium - Tree Data Structure with Traversal
Before (Haskell):
module Tree where
data Tree a = Leaf a
| Node a (Tree a) (Tree a)
deriving (Show, Eq)
-- Insert into binary search tree
insert :: Ord a => a -> Tree a -> Tree a
insert x (Leaf y)
| x < y = Node y (Leaf x) (Leaf y)
| otherwise = Node y (Leaf y) (Leaf x)
insert x (Node y left right)
| x < y = Node y (insert x left) right
| otherwise = Node y left (insert x right)
-- Map over tree
instance Functor Tree where
fmap f (Leaf x) = Leaf (f x)
fmap f (Node x left right) =
Node (f x) (fmap f left) (fmap f right)
-- Fold tree
foldTree :: (a -> b -> b -> b) -> (a -> b) -> Tree a -> b
foldTree node leaf (Leaf x) = leaf x
foldTree node leaf (Node x left right) =
node x (foldTree node leaf left) (foldTree node leaf right)
-- Inorder traversal
inorder :: Tree a -> [a]
inorder (Leaf x) = [x]
inorder (Node x left right) =
inorder left ++ [x] ++ inorder right
After (F#):
module Tree
type Tree<'a> =
| Leaf of 'a
| Node of 'a * Tree<'a> * Tree<'a>
// Insert into binary search tree
let rec insert (x: 'a) (tree: Tree<'a>) : Tree<'a> =
match tree with
| Leaf y ->
if x < y then Node(y, Leaf x, Leaf y)
else Node(y, Leaf y, Leaf x)
| Node(y, left, right) ->
if x < y then Node(y, insert x left, right)
else Node(y, left, insert x right)
// Map over tree
let rec map (f: 'a -> 'b) (tree: Tree<'a>) : Tree<'b> =
match tree with
| Leaf x -> Leaf (f x)
| Node(x, left, right) ->
Node(f x, map f left, map f right)
// Fold tree
let rec fold (nodeF: 'a -> 'b -> 'b -> 'b) (leafF: 'a -> 'b) (tree: Tree<'a>) : 'b =
match tree with
| Leaf x -> leafF x
| Node(x, left, right) ->
nodeF x (fold nodeF leafF left) (fold nodeF leafF right)
// Inorder traversal
let rec inorder (tree: Tree<'a>) : 'a list =
match tree with
| Leaf x -> [x]
| Node(x, left, right) ->
inorder left @ [x] @ inorder right
Example 3: Complex - Parser Combinator
Before (Haskell):
{-# LANGUAGE ApplicativeDo #-}
module Parser where
import Control.Applicative
import Data.Char
newtype Parser a = Parser { runParser :: String -> Maybe (a, String) }
instance Functor Parser where
fmap f (Parser p) = Parser $ \input -> do
(x, rest) <- p input
return (f x, rest)
instance Applicative Parser where
pure x = Parser $ \input -> Just (x, input)
Parser pf <*> Parser px = Parser $ \input -> do
(f, rest1) <- pf input
(x, rest2) <- px rest1
return (f x, rest2)
instance Alternative Parser where
empty = Parser $ const Nothing
Parser p1 <|> Parser p2 = Parser $ \input ->
p1 input <|> p2 input
-- Basic parsers
satisfy :: (Char -> Bool) -> Parser Char
satisfy pred = Parser $ \input ->
case input of
[] -> Nothing
(x:xs) -> if pred x then Just (x, xs) else Nothing
char :: Char -> Parser Char
char c = satisfy (== c)
string :: String -> Parser String
string [] = pure []
string (c:cs) = do
char c
string cs
pure (c:cs)
-- JSON-like parser
data Value = VNull
| VBool Bool
| VNumber Double
| VString String
| VArray [Value]
deriving (Show, Eq)
parseNull :: Parser Value
parseNull = string "null" *> pure VNull
parseBool :: Parser Value
parseBool = (string "true" *> pure (VBool True))
<|> (string "false" *> pure (VBool False))
parseNumber :: Parser Value
parseNumber = VNumber . read <$> some (satisfy isDigit)
parseString :: Parser Value
parseString = VString <$> (char '"' *> many (satisfy (/= '"')) <* char '"')
parseValue :: Parser Value
parseValue = parseNull <|> parseBool <|> parseNumber <|> parseString
After (F#):
module Parser
type Parser<'a> = Parser of (string -> ('a * string) option)
let runParser (Parser p) input = p input
// Functor (map)
let map (f: 'a -> 'b) (Parser p: Parser<'a>) : Parser<'b> =
Parser (fun input ->
match p input with
| Some(x, rest) -> Some(f x, rest)
| None -> None)
// Applicative (pure and apply)
let pure' (x: 'a) : Parser<'a> =
Parser (fun input -> Some(x, input))
let apply (Parser pf: Parser<'a -> 'b>) (Parser px: Parser<'a>) : Parser<'b> =
Parser (fun input ->
match pf input with
| Some(f, rest1) ->
match px rest1 with
| Some(x, rest2) -> Some(f x, rest2)
| None -> None
| None -> None)
// Alternative (empty and or)
let empty<'a> : Parser<'a> =
Parser (fun _ -> None)
let orElse (Parser p1: Parser<'a>) (Parser p2: Parser<'a>) : Parser<'a> =
Parser (fun input ->
match p1 input with
| Some result -> Some result
| None -> p2 input)
// Basic parsers
let satisfy (pred: char -> bool) : Parser<char> =
Parser (fun input ->
match Seq.tryHead input with
| Some c when pred c ->
Some(c, input.Substring(1))
| _ -> None)
let char' (c: char) : Parser<char> =
satisfy ((=) c)
let rec string' (s: string) : Parser<string> =
if s.Length = 0 then
pure' ""
else
Parser (fun input ->
if input.StartsWith(s) then
Some(s, input.Substring(s.Length))
else
None)
// JSON-like parser
type Value =
| VNull
| VBool of bool
| VNumber of float
| VString of string
| VArray of Value list
let parseNull: Parser<Value> =
string' "null"
|> map (fun _ -> VNull)
let parseBool: Parser<Value> =
orElse
(string' "true" |> map (fun _ -> VBool true))
(string' "false" |> map (fun _ -> VBool false))
let parseNumber: Parser<Value> =
Parser (fun input ->
let digits = input |> Seq.takeWhile System.Char.IsDigit |> Seq.toArray
if digits.Length > 0 then
let numStr = System.String(digits)
let num = float numStr
Some(VNumber num, input.Substring(digits.Length))
else
None)
let parseString: Parser<Value> =
Parser (fun input ->
if input.StartsWith("\"") then
let endIndex = input.IndexOf('"', 1)
if endIndex > 0 then
let content = input.Substring(1, endIndex - 1)
Some(VString content, input.Substring(endIndex + 1))
else
None
else
None)
let parseValue: Parser<Value> =
parseNull
|> orElse parseBool
|> orElse parseNumber
|> orElse parseString
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational conversion patterns (APTV workflow, testing strategies)lang-haskell-dev- Haskell development patternslang-fsharp-dev- F# development patternspatterns-concurrency-dev- Async, threads, STM across languagespatterns-serialization-dev- JSON, validation across languagespatterns-metaprogramming-dev- Template Haskell → Type Providers