PONY λ M2 Modula-2

Haskell.CodeCompared.To/F#

An interactive executable cheatsheet comparing Haskell and F#

GHC 9.12 F# (.NET 9)
Output & Basics
Hello, World
main :: IO () main = putStrLn "Hello, World!"
printfn "Hello, World!"
Haskell requires a named main :: IO () entry point that the runtime calls once. F# has no such requirement at the top level of a script — a source file is just a sequence of bindings and expressions evaluated in order, so printfn can be the first and only line.
Sequencing multiple outputs
main :: IO () main = do putStrLn "First" putStrLn "Second" putStrLn "Third"
printfn "First" printfn "Second" printfn "Third"
Haskell needs do-notation to sequence IO actions, since it desugars to chained monadic binds under the hood. F# is strict, so top-level statements simply execute one after another in the order written — no monad or block syntax required for plain sequencing.
Formatted output
import Text.Printf (printf) main :: IO () main = printf "%s is %d years old\n" "Ada" (36 :: Int)
printfn "%s is %d years old" "Ada" 36
Both languages provide a type-checked printf-style formatter. Haskell's Text.Printf.printf checks the format string against the argument types at compile time via a typeclass trick; F#'s printfn does the same natively as a compiler-recognized function, with no import needed.
Printing a value for inspection
main :: IO () main = print (Just [1, 2, 3])
printfn "%A" (Some [1; 2; 3])
Haskell's print uses the Show typeclass, deriving a textual representation for (almost) any value. F#'s %A format specifier does the equivalent job structurally, without requiring the type to opt in to anything — both exist so you rarely hand-write a formatter for ordinary data.
Values & Bindings
Let bindings
main :: IO () main = let radius = 5.0 area = pi * radius * radius in print area
let radius = 5.0 let area = System.Math.PI * radius * radius printfn "%f" area
Haskell's let ... in introduces bindings scoped to the following expression. F#'s top-level let is similar but does not need an explicit in — each binding is simply available to every subsequent line in the same scope.
Immutable by default in both languages
main :: IO () main = let count = 0 -- count = 1 below would be a NEW binding shadowing the old one, -- not a mutation — Haskell has no assignment operator at all: in print count
let count = 0 // count <- 1 below would be a compile error — "count" is not mutable // unless declared "let mutable count = 0" instead: printfn "%d" count
Both languages default every binding to immutable. Haskell has no assignment operator whatsoever — "changing" a value always means introducing a new binding. F# is slightly more permissive: it has a real assignment operator, <-, but only for bindings explicitly declared with the mutable keyword.
Opting in to mutation
import Data.IORef main :: IO () main = do ref <- newIORef 0 modifyIORef ref (+1) value <- readIORef ref print value
let mutable count = 0 count <- count + 1 printfn "%d" count
To get real mutable state, Haskell reaches for IORef (or STRef/MVar) and threads it through IO explicitly with newIORef/modifyIORef/readIORef. F#'s mutable keyword offers a plain, in-place mutable local variable directly — no wrapper type, no monad required.
Types & Inference
Both infer types via Hindley-Milner
double :: Int -> Int double x = x * 2 main :: IO () main = print (double 21)
let double x = x * 2 printfn "%d" (double 21)
Both languages use a Hindley-Milner-style inference algorithm, so explicit type signatures are usually optional — F#'s double here has no annotation at all, and the compiler infers int -> int from how x is used, exactly like Haskell would infer Int -> Int. This is one of the strongest structural parallels between the two languages.
Explicit type annotations
describe :: Int -> String describe n | n > 0 = "positive" | otherwise = "non-positive" main :: IO () main = putStrLn (describe 5)
let describe (n: int) : string = if n > 0 then "positive" else "non-positive" printfn "%s" (describe 5)
Haskell places a function's type signature on its own line above the definition, using ::. F# annotates parameters and return types inline, in parentheses, using :. Both are optional whenever inference can work it out on its own; both become mandatory in cases inference cannot resolve, such as polymorphic recursion.
Generic functions
firstOf :: [a] -> Maybe a firstOf [] = Nothing firstOf (x:_) = Just x main :: IO () main = print (firstOf [1, 2, 3])
let firstOf list = match list with | [] -> None | head :: _ -> Some head printfn "%A" (firstOf [1; 2; 3])
Neither language needs anything special written to make a function generic — Haskell's lowercase type variable a and F#'s inferred generic parameter both fall out automatically from a definition that never constrains the element type. Both would display the inferred generic signature identically if asked ([a] -> Maybe a vs 'a list -> 'a option).
Strings
String concatenation
main :: IO () main = putStrLn ("Hello, " ++ "World!")
printfn "%s" ("Hello, " + "World!")
Haskell's ++ is the general list-concatenation operator, since a String is really [Char]. F#'s + is operator-overloaded specifically for System.String concatenation — a genuine .NET string, not a character list.
String interpolation
import Text.Printf (printf) main :: IO () main = do let name = "Ada" age = 36 :: Int printf "%s is %d\n" name age
let name = "Ada" let age = 36 printfn $"{name} is {age}"
Haskell has no native string-interpolation syntax — the idiomatic route is Text.Printf with positional format specifiers. F# 9 supports true interpolated strings with the $"..." prefix, embedding expressions directly in { } placeholders, closer to Python f-strings or Ruby's #{}.
A string is a list of characters — or a real class
main :: IO () main = print (reverse "hello")
let value = "hello" printfn "%s" (System.String(Array.rev (value.ToCharArray())))
Haskell's String is literally a type alias for [Char], so ordinary list functions like reverse apply directly. F#'s string is a real .NET System.String class, not a list — reversing it means converting to a character array first, reversing that, and rebuilding the string, a small but real ergonomic cost for the stronger .NET string guarantees (interning, culture-aware comparisons, and so on).
Collections
Lists and the cons pattern
main :: IO () main = let (first:rest) = [1, 2, 3] in print (first, rest)
let (first :: rest) = [1; 2; 3] printfn "%A" (first, rest)
Both languages model lists as singly-linked cons cells and use nearly identical syntax to destructure one: Haskell's (first:rest) versus F#'s (first :: rest), the same :: operator lists are built from. Both require every element to share one type.
Map and filter
main :: IO () main = let doubled = map (* 2) [1, 2, 3, 4, 5] evens = filter even doubled in print evens
let doubled = List.map (fun n -> n * 2) [1; 2; 3; 4; 5] let evens = List.filter (fun n -> n % 2 = 0) doubled printfn "%A" evens
Both languages call these operations map and filter, with function first and list second in both. F#'s versions live under the List module namespace (List.map), while Haskell's are unqualified top-level functions from the Prelude.
Folding a list
main :: IO () main = print (foldl (+) 0 [1, 2, 3, 4, 5])
let total = List.fold (+) 0 [1; 2; 3; 4; 5] printfn "%d" total
Haskell's foldl and F#'s List.fold take the same three arguments in the same order: function, initial accumulator, list. This is one of the closest standard-library vocabulary matches between the two languages.
Key/value maps
import qualified Data.Map as Map main :: IO () main = let ages = Map.fromList [("Ada", 36), ("Alan", 41)] in print (Map.lookup "Ada" ages)
let ages = Map.ofList [("Ada", 36); ("Alan", 41)] printfn "%A" (Map.tryFind "Ada" ages)
Both languages provide an immutable, balanced-tree map as an opt-in module: Haskell's Data.Map (usually imported qualified, since names like lookup clash with the Prelude) and F#'s built-in Map module, which needs no import at all. Map.lookup and Map.tryFind both return an optional value rather than throwing on a missing key.
Control Flow
if is an expression in both languages
main :: IO () main = let n = 5 message = if n > 0 then "positive" else "non-positive" in putStrLn message
let n = 5 let message = if n > 0 then "positive" else "non-positive" printfn "%s" message
In both languages, if is an expression that produces a value, not a statement — the else branch is mandatory in both, since without it the expression would have no value to produce on the false path.
Guards
classify :: Int -> String classify n | n == 0 = "zero" | n > 0 = "positive" | otherwise = "negative" main :: IO () main = putStrLn (classify (-5))
let classify n = if n = 0 then "zero" elif n > 0 then "positive" else "negative" printfn "%s" (classify -5)
Haskell's guard syntax (| condition = result) reads like a sequence of conditions checked top to bottom, ending in otherwise. F# has no direct guard syntax outside of match — the same idea is expressed with a chained if/elif/else, or with a match using when guards (see Pattern Matching).
No native loop construct — recursion instead
countdown :: Int -> IO () countdown 0 = putStrLn "liftoff!" countdown n = do print n countdown (n - 1) main :: IO () main = countdown 3
let rec countdown n = if n = 0 then printfn "liftoff!" else printfn "%d" n countdown (n - 1) countdown 3
Neither language has a native for/while loop that mutates a counter the way an imperative language would — recursion is the idiomatic control-flow tool in both. (F# does offer for/while as syntax over .NET-style iteration and mutable state, but recursion is preferred whenever the loop body is not simply iterating a sequence.)
Pattern Matching
case versus match
describe :: Int -> String describe n = case n of 0 -> "zero" n | n > 0 -> "positive" _ -> "negative" main :: IO () main = putStrLn (describe (-5))
let describe n = match n with | 0 -> "zero" | n when n > 0 -> "positive" | _ -> "negative" printfn "%s" (describe -5)
Haskell's case ... of and F#'s match ... with are close enough to read as dialects of the same construct: patterns tried top to bottom, an optional guard attached to a pattern (Haskell reuses |, F# uses when), and a wildcard _ catch-all.
Both compilers warn on non-exhaustive matches
-- GHC emits a warning here (with -Wincomplete-patterns) since -- the negative case is missing entirely: classify :: Int -> String classify 0 = "zero" classify n | n > 0 = "positive" main :: IO () main = putStrLn (classify 5)
let classify n = match n with | 0 -> "zero" | n when n > 0 -> "positive" // F# emits a compiler WARNING here too — "Incomplete pattern // matches on this expression" — if the negative case were missing. | _ -> "negative" printfn "%s" (classify 5)
This is a genuine shared strength. Both compilers statically analyze whether a pattern match covers every possible case and warn (and can be configured to error) when it does not — catching a whole class of runtime crash before the program ever executes, unlike languages whose pattern matching fails silently or only at the missed input.
Destructuring a tuple
main :: IO () main = let point = (3, 4) (x, y) = point in print (x + y)
let point = (3, 4) let (x, y) = point printfn "%d" (x + y)
Tuple destructuring is essentially identical syntax in both languages — a pattern of parenthesized, comma-separated names on the left of a binding, matched positionally against the tuple's shape.
Functions & Currying
Every function is curried automatically in both languages
add :: Int -> Int -> Int add x y = x + y main :: IO () main = let addFive = add 5 in print (addFive 10)
let add x y = x + y let addFive = add 5 printfn "%d" (addFive 10)
Unlike most languages compared elsewhere on this site, F# genuinely matches Haskell here: every multi-argument function is secretly a chain of one-argument functions, so add 5 alone is already valid and returns a new function awaiting the second argument, with no special syntax needed in either language.
Anonymous functions (lambdas)
main :: IO () main = print (map (\x -> x * x) [1, 2, 3, 4])
printfn "%A" (List.map (fun x -> x * x) [1; 2; 3; 4])
Haskell spells a lambda \x -> ... (the backslash evokes the Greek letter lambda); F# spells the same thing fun x -> .... Both are ordinary expressions, not a special statement form, so both can be passed anywhere a function value is expected.
Function composition
main :: IO () main = let addOneThenDouble = (* 2) . (+ 1) in print (addOneThenDouble 5)
let addOneThenDouble = (fun x -> x + 1) >> (fun x -> x * 2) printfn "%d" (addOneThenDouble 5)
Haskell's . composes right-to-left, mathematical-notation style: (f . g) x means f (g x). F#'s >> composes left-to-right, matching the order the functions are written and read — (f >> g) x means g (f x), arguably the more readable convention of the two.
The pipe operator
import Data.Function ((&)) main :: IO () main = [1, 2, 3, 4, 5] & map (* 2) & filter even & print
[1; 2; 3; 4; 5] |> List.map (fun n -> n * 2) |> List.filter (fun n -> n % 2 = 0) |> printfn "%A"
F#'s |> is idiomatic and built into every F# codebase; Haskell's equivalent, & from Data.Function, exists but is used far less often — most Haskell code prefers composing with . or nesting calls instead, so a Haskeller reading F# code will see |> chains far more than the Haskell community's own pipe operator.
Maybe/Either vs. Option/Result
Maybe becomes Option
import qualified Data.Map as Map main :: IO () main = let ages = Map.fromList [("Ada", 36)] in print (Map.lookup "Alan" ages)
let ages = Map.ofList [("Ada", 36)] printfn "%A" (Map.tryFind "Alan" ages)
Haskell's Maybe a (Just x / Nothing) and F#'s 'T option (Some x / None) are the same idea with different names — both replace null with an explicit, compiler-enforced "value or absence" type.
Either becomes Result
divide :: Int -> Int -> Either String Int divide _ 0 = Left "divide by zero" divide x y = Right (x `div` y) main :: IO () main = print (divide 10 0)
let divide x y = if y = 0 then Error "divide by zero" else Ok (x / y) printfn "%A" (divide 10 0)
Haskell's Either String Int (Left for failure, Right for success, by convention) is renamed and reordered in F#'s Result<int, string> (Ok for success, Error for failure) — same two-case sum type carrying a reason on failure, just with the success/failure case order and names swapped.
Skipping the failure case is a compile error in both
divide :: Int -> Int -> Either String Int divide _ 0 = Left "divide by zero" divide x y = Right (x `div` y) main :: IO () main = case divide 10 0 of Right value -> print value Left reason -> putStrLn ("Error: " ++ reason)
let divide x y = if y = 0 then Error "divide by zero" else Ok (x / y) match divide 10 0 with | Ok value -> printfn "%d" value | Error reason -> printfn "Error: %s" reason
Both compilers reject treating an Either/Result as if it were the bare success value — pattern-matching both branches (or explicitly using a helper like fromRight/Result.defaultValue that forces a decision about the failure case) is the only way to get the value out in either language.
Records
Defining a record
data Point = Point { x :: Int, y :: Int } deriving (Show) main :: IO () main = let point = Point { x = 3, y = 4 } in print (x point, y point)
type Point = { X: int; Y: int } let point = { X = 3; Y = 4 } printfn "%d and %d" point.X point.Y
Both define a named product type with labeled fields and access them with dot-like syntax — Haskell generates a top-level accessor function per field (x :: Point -> Int), which is why x point reads as ordinary function application; F#'s point.X uses genuine dot-member access, closer to how most other languages read records.
Record update syntax
data Point = Point { x :: Int, y :: Int } deriving (Show) main :: IO () main = let point = Point { x = 3, y = 4 } moved = point { y = 99 } in print moved
type Point = { X: int; Y: int } let point = { X = 3; Y = 4 } let moved = { point with Y = 99 } printfn "%A" moved
The syntax is near-identical: Haskell's point { y = 99 } and F#'s { point with Y = 99 } both produce a new record with one field replaced and every other field copied automatically — neither language can mutate the original record in place.
Field-name collisions between records
-- Before DuplicateRecordFields/OverloadedRecordDot, two records in the -- same module could not both have a field named "name" without qualifying -- them. Modern GHC still requires explicit extensions for this to -- resolve automatically: {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedRecordDot #-} data Person = Person { name :: String } data Company = Company { name :: String } main :: IO () main = do let person = Person { name = "Ada" } putStrLn person.name
type Person = { Name: string } type Company = { Name: string } let person: Person = { Name = "Ada" } printfn "%s" person.Name
This is a real historical wart in Haskell: two records with a field of the same name in the same module clash unless the DuplicateRecordFields extension is enabled, and even then, plain function-style access like name person stays ambiguous — resolving it also requires the OverloadedRecordDot extension's person.name syntax. F# has no such restriction at all — record field names are scoped to their own type from the start, and person.Name dot access has always worked with no extra extension.
Algebraic Data Types vs. Discriminated Unions
Modeling "one of several shapes"
data Shape = Circle Double | Rectangle Double Double area :: Shape -> Double area (Circle radius) = pi * radius * radius area (Rectangle width height) = width * height main :: IO () main = print (map area [Circle 5.0, Rectangle 3.0 4.0])
type Shape = | Circle of float | Rectangle of float * float let area shape = match shape with | Circle radius -> System.Math.PI * radius * radius | Rectangle (width, height) -> width * height printfn "%A" (List.map area [Circle 5.0; Rectangle (3.0, 4.0)])
Haskell's algebraic data types and F#'s discriminated unions are effectively the same feature under different names — both declare a closed set of tagged variants, each optionally carrying its own payload, and both are deconstructed with pattern matching that the compiler checks for exhaustiveness.
Recursive data types
data Tree = Leaf | Node Tree Int Tree sumTree :: Tree -> Int sumTree Leaf = 0 sumTree (Node left value right) = sumTree left + value + sumTree right main :: IO () main = print (sumTree (Node (Node Leaf 1 Leaf) 2 (Node Leaf 3 Leaf)))
type Tree = | Leaf | Node of Tree * int * Tree let rec sumTree tree = match tree with | Leaf -> 0 | Node (left, value, right) -> sumTree left + value + sumTree right printfn "%d" (sumTree (Node (Node (Leaf, 1, Leaf), 2, Node (Leaf, 3, Leaf))))
A recursive sum type referencing itself — the classic binary tree — is written almost identically in both languages, right down to needing rec (F#) versus nothing extra (Haskell, where every top-level binding may already refer to itself) to define the traversal function.
A zero-cost wrapper: newtype versus a single-case union
newtype UserId = UserId Int unwrap :: UserId -> Int unwrap (UserId n) = n main :: IO () main = print (unwrap (UserId 42))
type UserId = UserId of int let unwrap (UserId n) = n printfn "%d" (unwrap (UserId 42))
Haskell's newtype creates a wrapper type that is guaranteed to be erased at compile time — zero runtime overhead, but the type checker still keeps a UserId distinct from a bare Int. F# has no dedicated zero-cost-wrapper keyword; a single-case discriminated union (type UserId = UserId of int) is the idiomatic substitute, though it is not guaranteed to be erased the way newtype is.
Type Classes vs. Interfaces & SRTP
Type classes have no clean F# equivalent
class Describable a where describe :: a -> String data Dog = Dog instance Describable Dog where describe _ = "a dog" main :: IO () main = putStrLn (describe Dog)
type IDescribable = abstract member Describe: unit -> string type Dog() = interface IDescribable with member _.Describe() = "a dog" let dog = Dog() :> IDescribable printfn "%s" (dog.Describe())
This is the deepest structural difference between the two languages. Haskell resolves "which implementation for which type" through type classes, with the compiler picking the right instance automatically based purely on the type at the call site — no explicit upcast anywhere. F# has no type classes at all; the nearest analogue is an object-oriented interface, which requires the implementing type to declare conformance up front (interface IDescribable with) and, as shown here, an explicit cast (:> IDescribable) to call through the interface — considerably more ceremony than Haskell's automatic dispatch.
Statically Resolved Type Parameters: F#'s closest analogue
class Zero a where zero :: a instance Zero Int where zero = 0 instance Zero Double where zero = 0.0 main :: IO () main = print (zero :: Int)
let inline zero< ^T when ^T: (static member Zero: ^T) > : ^T = (^T: (static member Zero: ^T) ()) printfn "%d" (zero<int>)
F#'s Statically Resolved Type Parameters (SRTP) — inline functions constrained with ^T: (static member ...) — are the feature that comes closest to Haskell type-class-style ad-hoc polymorphism, letting a function be generic over "any type with a matching static member." It is considerably more verbose and far less commonly used than Haskell's type classes, which are the default tool for this kind of polymorphism rather than a specialist escape hatch.
Deriving Eq/Ord/Show versus automatic structural equality
data Point = Point Int Int deriving (Eq, Show) main :: IO () main = print (Point 1 2 == Point 1 2)
type Point = Point of int * int printfn "%b" (Point (1, 2) = Point (1, 2))
Haskell requires an explicit deriving (Eq) clause to opt a type into structural equality (implemented, mechanically, as an automatically-generated type class instance). F# discriminated unions and records get structural equality, comparison, and hashing automatically, with no annotation needed at all — one case where F#'s default behavior is more convenient than Haskell's.
Laziness vs. Strictness
Infinite lists exist only because Haskell is lazy
main :: IO () main = print (take 5 [1..])
let naturals = Seq.initInfinite (fun n -> n + 1) printfn "%A" (Seq.take 5 naturals |> Seq.toList)
Haskell's [1..] is an ordinary list — it only works because Haskell is lazy by default, generating each element on demand as take asks for it. F# lists are strict and would try to build the entire infinite list immediately (and hang forever); F#'s lazily-evaluated seq is the type that plays Haskell's "infinite list" role instead.
Lazy is the exception in F#, not the default
main :: IO () main = do let expensive = 2 ^ 100 -- not computed until actually demanded putStrLn "before use" print expensive
let expensive = lazy (2.0 ** 100.0) // wrapped explicitly with "lazy" printfn "before use" printfn "%f" expensive.Value // .Value forces the computation, once
In Haskell, every binding is lazy unless forced otherwise — expensive is not actually computed until print demands it. F# is strict by default, so achieving the same deferred, compute-once-and-cache behavior requires the explicit lazy keyword and an explicit .Value to force it — laziness is opt-in machinery in F#, not the ambient evaluation model.
F#'s evaluation order is predictable; Haskell's is not
main :: IO () main = do -- Order of evaluation for pure (non-IO) expressions is NOT guaranteed -- to be left-to-right — Haskell only guarantees the IO actions -- themselves run top-to-bottom: let pair = (trace "left" 1, trace "right" 2) print pair where trace label value = value -- stand-in; Debug.Trace would print as a side effect
// F# evaluates strictly and left-to-right, so side effects inside an // expression happen in the order they are written, every time: let pair = (printfn "left"; 1), (printfn "right"; 2) printfn "%A" pair
Because Haskell is lazy, the evaluation order of pure sub-expressions is officially unspecified — only the sequencing of actual IO actions is guaranteed. F#, being strict, evaluates left-to-right and top-to-bottom deterministically, so a side effect embedded in an expression (as shown here purely for illustration — not idiomatic style in either language) always happens in a predictable order.
Monads & do-notation vs. Computation Expressions
do-notation for IO
main :: IO () main = do putStrLn "What is your name?" let name = "Ada" -- stand-in for getLine, to keep this example self-contained putStrLn ("Hello, " ++ name ++ "!")
printfn "What is your name?" let name = "Ada" // stand-in for System.Console.ReadLine(), to keep this self-contained printfn "Hello, %s!" name
Haskell's do-notation is syntactic sugar over the general Monad typeclass — it works identically for IO, Maybe, lists, parsers, and any other type with a Monad instance. F# has no single generalized monad abstraction; ordinary strict top-level code already sequences IO-like actions without needing any special notation at all, since there is no need to thread an IO value through pure code the way Haskell does.
The Maybe monad versus explicit Option matching
import qualified Data.Map as Map lookupBoth :: String -> String -> Map.Map String Int -> Maybe Int lookupBoth keyOne keyTwo table = do first <- Map.lookup keyOne table second <- Map.lookup keyTwo table return (first + second) main :: IO () main = let table = Map.fromList [("a", 1), ("b", 2)] in print (lookupBoth "a" "b" table)
let lookupBoth keyOne keyTwo table = match Map.tryFind keyOne table with | None -> None | Some first -> match Map.tryFind keyTwo table with | None -> None | Some second -> Some (first + second) let table = Map.ofList [("a", 1); ("b", 2)] printfn "%A" (lookupBoth "a" "b" table)
Haskell's do-notation over Maybe short-circuits automatically on the first Nothing, via the generic Monad typeclass — no explicit branching visible at all. F# has no equivalent generalized monadic bind for Option built into the core syntax, so the same short-circuiting is written out as nested match expressions (or, in real F# codebases, a hand-written option { } computation expression — see the next example).
Computation expressions are type-specific, not generalized
-- Haskell's do-notation works UNCHANGED for any Monad — IO, Maybe, -- lists, or a custom type — because Monad is one typeclass with one -- bind operation (>>=) that every instance implements: main :: IO () main = do let doubled = do x <- [1, 2, 3] return (x * 2) print (doubled :: [Int])
// F# has no single generalized monad — "async { }", "seq { }", and a // hand-written "option { }" are each their OWN separate computation // expression, each with its own builder type implementing its own // Bind/Return members. They look similar but are NOT interchangeable: let doubled = seq { for x in [1; 2; 3] do yield x * 2 } printfn "%A" (Seq.toList doubled)
This is the sharpest practical difference between the two languages' approach to "do-notation-like" syntax. Haskell's Monad is one typeclass; any type that implements >>= gets do-notation for free, and the same block of syntax works for IO today and a custom parser type tomorrow. F#'s computation expressions (async { }, seq { }, a hand-rolled option { }) are separate, unrelated builder types that merely share superficially similar block syntax — there is no single abstraction unifying them the way Monad unifies Haskell's.
Error Handling
Exceptions exist in both, but are secondary to Maybe/Result
import Control.Exception main :: IO () main = do result <- try (evaluate (1 `div` 0)) :: IO (Either ArithException Int) case result of Left err -> putStrLn ("Caught: " ++ show err) Right value -> print value
try let result = 1 / 0 printfn "%d" result with | :? System.DivideByZeroException as ex -> printfn "Caught: %s" ex.Message
Both languages support exceptions (Haskell's Control.Exception, F#'s inherited .NET try/with), but idiomatic code in both prefers modeling expected failure explicitly with Maybe/Either or Option/Result, reserving exceptions for genuinely unexpected, unrecoverable situations.
Modeling errors as data instead of exceptions
data ValidationError = TooShort | TooLong deriving (Show) validate :: String -> Either ValidationError String validate name | length name < 2 = Left TooShort | length name > 20 = Left TooLong | otherwise = Right name main :: IO () main = print (validate "A")
type ValidationError = TooShort | TooLong let validate (name: string) = if name.Length < 2 then Error TooShort elif name.Length > 20 then Error TooLong else Ok name printfn "%A" (validate "A")
Both languages favor representing "what can go wrong" as an ordinary data type — a Haskell ADT or an F# discriminated union — carried in Either/Result, rather than throwing. This makes every possible failure visible in the function's type signature, and the compiler enforces that callers handle it.
Classic Examples
Factorial
factorial :: Integer -> Integer factorial 0 = 1 factorial n = n * factorial (n - 1) main :: IO () main = print (factorial 10)
let rec factorial n = if n = 0 then 1L else int64 n * factorial (n - 1) printfn "%d" (factorial 10)
A base-case clause and a recursive clause, nearly a mechanical translation between the two. F# needs the explicit rec keyword to mark factorial as self-referencing; an ordinary Haskell binding may always refer to itself with no special marker.
Fibonacci with memoization
fibs :: [Integer] fibs = 0 : 1 : zipWith (+) fibs (tail fibs) main :: IO () main = print (fibs !! 30)
let fibonacci n = let cache = System.Collections.Generic.Dictionary<int, int64>() let rec fib n = if n <= 1 then int64 n else match cache.TryGetValue n with | true, value -> value | false, _ -> let value = fib (n - 1) + fib (n - 2) cache.[n] <- value value fib n printfn "%d" (fibonacci 30)
Haskell's laziness turns memoization into a one-liner: fibs is a genuinely infinite, self-referential list, and because each element is only computed once and then cached automatically as a thunk, indexing it repeatedly is efficient with no explicit cache at all. F#, being strict, needs an explicit mutable dictionary to get the same memoized behavior — laziness-as-free-memoization is one of Haskell's most celebrated idioms, and it has no equally terse F# equivalent.
Quicksort
quicksort :: (Ord a) => [a] -> [a] quicksort [] = [] quicksort (pivot:rest) = quicksort smaller ++ [pivot] ++ quicksort larger where smaller = filter (< pivot) rest larger = filter (>= pivot) rest main :: IO () main = print (quicksort [3, 1, 4, 1, 5, 9, 2, 6])
let rec quicksort list = match list with | [] -> [] | pivot :: rest -> let smaller = List.filter (fun n -> n < pivot) rest let larger = List.filter (fun n -> n >= pivot) rest quicksort smaller @ [pivot] @ quicksort larger printfn "%A" (quicksort [3; 1; 4; 1; 5; 9; 2; 6])
The famous "quicksort in three lines" example translates almost line for line: pattern-match the empty list and the pivot-plus-rest case, filter into smaller/larger partitions, recurse, and concatenate. Haskell's ++ and F#'s @ both mean list concatenation.