PONY λ M2 Modula-2

Haskell.CodeCompared.To/Swift

An interactive executable cheatsheet comparing Haskell and Swift

GHC 9.12 Swift 6.3
Output & Basics
Hello, World
main :: IO () main = putStrLn "Hello, World!"
print("Hello, World!")
Haskell requires a named main :: IO () entry point that the runtime calls once. Swift, in its top-level-script mode (used for every example on this page, matching the whole site's Swift convention), needs no main at all — print can be the first and only line, statements just run top to bottom.
Sequencing multiple outputs
main :: IO () main = do putStrLn "First" putStrLn "Second" putStrLn "Third"
print("First") print("Second") print("Third")
Haskell needs do-notation to sequence IO actions, since it desugars to chained monadic binds. Swift is strict, so top-level statements simply execute one after another in the order written — no monad or block syntax required for plain sequencing.
Printing a value for inspection
main :: IO () main = print (Just [1, 2, 3])
print(Optional([1, 2, 3]))
Haskell's print uses the Show typeclass, deriving a textual representation for (almost) any value automatically. Swift's print relies on the CustomStringConvertible protocol (which most standard types conform to already) — the output Optional([1, 2, 3]) needs no extra formatting call, much like Haskell's derived Show.
Values & Bindings
let is Haskell-style immutable binding
main :: IO () main = let radius = 5.0 area = pi * radius * radius in print area
import Foundation let radius = 5.0 let area = Double.pi * radius * radius print(area)
Haskell's let ... in bindings and Swift's let both bind an immutable name once — reassigning a Swift let is a compile error, matching Haskell's single-assignment discipline exactly, unlike Swift's separate var (next row).
Opting in to mutation with var
import Data.IORef main :: IO () main = do ref <- newIORef 0 modifyIORef ref (+1) value <- readIORef ref print value
var count = 0 count += 1 print(count)
Haskell reaches for IORef and threads it explicitly through IO with newIORef/modifyIORef/readIORef. Swift's var keyword offers a plain, in-place mutable local variable directly — no wrapper type, no monad required, and — unlike Haskell — mutation is a first-class, everyday tool in idiomatic Swift rather than an explicit opt-in reserved for special cases.
Types & Inference
Local inference feels familiar; signatures do not
double :: Int -> Int double x = x * 2 main :: IO () main = print (double 21)
func double(_ x: Int) -> Int { x * 2 } print(double(21))
Swift infers the type of local values the way Haskell does, so everyday code reads almost as unannotated as Haskell. The difference surfaces at function boundaries: Swift parameter types and return types are always explicit, the one place Haskell's inference goes further than Swift's.
Generic functions
firstOf :: [a] -> Maybe a firstOf [] = Nothing firstOf (x:_) = Just x main :: IO () main = print (firstOf [1, 2, 3])
func firstOf<T>(_ list: [T]) -> T? { list.first } print(firstOf([1, 2, 3]) as Any)
Haskell's lowercase type variable a and Swift's angle-bracketed <T> both introduce a generic type parameter, but Swift requires it to be written explicitly — Haskell infers that firstOf is generic purely from never constraining a, where Swift requires the <T> annotation even though the body needs no further help.
Strings
String concatenation
main :: IO () main = putStrLn ("Hello, " ++ "World!")
print("Hello, " + "World!")
Haskell's ++ is the general list-concatenation operator, since a String is really [Char]. Swift's + is dedicated String concatenation, since Swift's String is a genuine Unicode-correct value type, 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 print("\(name) is \(age)")
Haskell has no native interpolation — the idiomatic route is Text.Printf with positional format specifiers. Swift has true string interpolation built into the language with \( ) placeholders, closer to Python f-strings or Ruby's #{}, and needs no import.
A string is a list of characters — or a Unicode-correct value type
main :: IO () main = print (length "hello")
print("hello".count)
Haskell's String is literally a type alias for [Char], so ordinary list functions like length apply directly and count Unicode codepoints. Swift's String is a genuine Collection of extended grapheme clusters (user-perceived characters, not raw codepoints) — .count correctly counts a family emoji as one character even though it is composed of several Unicode scalars, something Haskell's naive codepoint-based length would get wrong.
Collections
Array literals
main :: IO () main = print [1, 2, 3, 4, 5]
print([1, 2, 3, 4, 5])
The literal syntax is identical. Under the hood the representations differ completely: a Haskell list is a lazy, singly-linked chain of cons cells, while Swift's Array is contiguous in memory, matching most mainstream languages rather than the functional-language linked-list convention.
map and filter
main :: IO () main = let doubled = map (* 2) [1, 2, 3, 4, 5] evens = filter even doubled in print evens
let doubled = [1, 2, 3, 4, 5].map { $0 * 2 } let evens = doubled.filter { $0 % 2 == 0 } print(evens)
Both languages call these operations map and filter. Swift calls them as methods on the collection using trailing-closure syntax (list.map { ... }), where Haskell calls them as free functions taking the list last (map f list) — the same operation, opposite argument-order convention.
Folding a collection
main :: IO () main = print (foldl (+) 0 [1, 2, 3, 4, 5])
let total = [1, 2, 3, 4, 5].reduce(0) { accumulator, number in accumulator + number } print(total)
Haskell's foldl and Swift's reduce take the same pieces — initial accumulator, then a combining function — with the same argument order (accumulator first). Swift additionally offers a shorthand reduce(0, +) passing the operator directly, when no naming is needed.
Key/value dictionaries
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 = ["Ada": 36, "Alan": 41] print(ages["Ada"] as Any)
Both languages provide a map/dictionary as a first-class collection: Haskell's Data.Map (usually imported qualified) and Swift's built-in Dictionary, with its own bracket-based literal syntax needing no import. Both subscript lookups already return an optional value rather than throwing on a missing key.
Maybe vs. Optional
Maybe becomes Optional, Just/Nothing become a value or nil
findUser :: Int -> Maybe String findUser 1 = Just "Ada" findUser _ = Nothing main :: IO () main = print (findUser 1)
func findUser(_ id: Int) -> String? { id == 1 ? "Ada" : nil } print(findUser(1) as Any)
Haskell's Maybe a (Just x/Nothing) and Swift's T? (any value, or nil) are the same idea, but Swift integrates optionality so deeply into the language syntax that T? feels like a primitive modifier rather than a wrapping type — where Haskell's Maybe is simply an ordinary algebraic data type from the standard library, with no special syntax support.
Unwrapping: if let versus case
main :: IO () main = let maybeValue = Just 42 in case maybeValue of Just value -> putStrLn ("Got " ++ show value) Nothing -> putStrLn "nothing"
let maybeValue: Int? = 42 if let value = maybeValue { print("Got \(value)") } else { print("nothing") }
Haskell unwraps a Maybe with an ordinary case expression, the same tool used for every other pattern match. Swift has a dedicated if let construct specifically for optionals — syntactically lighter than a full switch, though a Swift switch on .some(value)/.none would work identically, since Optional really is just an ordinary two-case enum underneath.
Defaults: fromMaybe versus ??
import Data.Maybe (fromMaybe) main :: IO () main = let maybeValue = Nothing :: Maybe Int in print (fromMaybe 0 maybeValue)
let maybeValue: Int? = nil print(maybeValue ?? 0)
Haskell's fromMaybe defaultValue maybeValue and Swift's maybeValue ?? defaultValue do the same job — use the value if present, fall back otherwise — but Swift expresses it as an infix nil-coalescing operator rather than a named function, the same ?? spelling used in C#, TypeScript, and Kotlin.
guard let: early-exit unwrapping with no Haskell parallel
-- Haskell has no direct equivalent construct — the closest is a -- guard inside a list comprehension, or pattern-matching in a -- `do`-block that fails into MonadFail: doubled :: Maybe Int -> Maybe Int doubled Nothing = Nothing doubled (Just value) = Just (value * 2) main :: IO () main = print (doubled (Just 21))
func doubled(_ optional: Int?) -> Int? { guard let value = optional else { return nil } return value * 2 } print(doubled(21) as Any)
Swift's guard let ... else { return } unwraps an optional and, if it is nil, forces an early exit right there — the unwrapped binding then stays in scope for the REST of the function, unlike if let's narrower scope. Haskell has no single construct that matches this shape; the closest equivalents (a pattern-matched function clause, or do-notation's implicit MonadFail) work quite differently under the hood.
Algebraic Data Types vs. Enums
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])
import Foundation enum Shape { case circle(Double) case rectangle(Double, Double) } func area(_ shape: Shape) -> Double { switch shape { case .circle(let radius): return Double.pi * radius * radius case .rectangle(let width, let height): return width * height } } print([Shape.circle(5.0), Shape.rectangle(3.0, 4.0)].map(area))
Haskell's algebraic data types and Swift's enums with associated values are close structural cousins — both declare a closed set of tagged, optionally payload-carrying variants, deconstructed with pattern matching the compiler checks for exhaustiveness. Swift needs the explicit case keyword per variant and dot-shorthand construction (.circle(5.0)) where Haskell just writes the bare constructor.
Payload-free constructors
data Direction = North | South | East | West deriving (Show) main :: IO () main = print North
enum Direction { case north, south, east, west } print(Direction.north)
A payload-free Haskell constructor and a payload-free Swift enum case translate almost mechanically — Swift additionally allows listing several cases on one comma-separated line (case north, south, east, west), a terser style than Haskell's pipe-separated data declaration but expressing the identical idea.
Recursive enums need the explicit `indirect` keyword
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)))
indirect enum Tree { case leaf case node(Tree, Int, Tree) } func sumTree(_ tree: Tree) -> Int { switch tree { case .leaf: return 0 case .node(let left, let value, let right): return sumTree(left) + value + sumTree(right) } } print(sumTree(.node(.node(.leaf, 1, .leaf), 2, .node(.leaf, 3, .leaf))))
Haskell needs no special marker for a self-referencing data type — every constructor is already heap-allocated behind a pointer. Swift enums are value types stored inline by default, so a directly self-referencing case would have unbounded size; the explicit indirect keyword tells the compiler to box that case (or the whole enum) behind a pointer instead, exactly the representation Haskell uses automatically and silently.
Pattern Matching
case versus switch
describe :: Int -> String describe n = case n of 0 -> "zero" n | n > 0 -> "positive" _ -> "negative" main :: IO () main = putStrLn (describe (-5))
func describe(_ n: Int) -> String { switch n { case 0: return "zero" case let n where n > 0: return "positive" default: return "negative" } } print(describe(-5))
Haskell's case ... of and Swift's switch { } read as dialects of the same construct: patterns tried top to bottom, a where-guard attachable to a pattern (Haskell reuses |, Swift the keyword where), and a catch-all (Haskell's _, Swift's default).
Both compilers check exhaustiveness — for closed types
data Direction = North | South | East | West deriving (Show) -- GHC warns here (with -Wincomplete-patterns) since a case is missing: describe :: Direction -> String describe North = "Heading north" describe South = "Heading south" describe East = "Heading east" main :: IO () main = putStrLn (describe North)
enum Direction { case north, south, east, west } func describe(_ direction: Direction) -> String { switch direction { case .north: return "Heading north" case .south: return "Heading south" case .east: return "Heading east" // Swift's compiler REJECTS this switch outright — "switch must // be exhaustive" — with .west missing, no warning-only mode: case .west: return "Heading west" } } print(describe(.north))
Both compilers verify a match covers every case for a closed enum/data type. Swift is actually stricter here than default GHC: a non-exhaustive switch over an enum is an unconditional compile ERROR in Swift, where GHC's equivalent gap is a configurable warning (elevated to an error only under stricter flags) unless a default case is present.
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 print(x + y)
Tuple destructuring is essentially identical syntax in both languages — a parenthesized, comma-separated pattern on the left of a binding, matched positionally against the tuple's shape.
Records vs. Structs
Records versus a genuine value-type struct
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)
struct Point { var x: Int var y: Int } let point = Point(x: 3, y: 4) print((point.x, point.y))
Both define a named product type with labeled fields, accessed with dot syntax. Haskell generates a top-level accessor function per field (x :: Point -> Int); Swift's struct gives genuine dot-member access plus a free memberwise initializer (Point(x:y:)) generated automatically from the field list.
"Updating" a value: record syntax versus mutate-a-copy
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
struct Point { var x: Int var y: Int } let point = Point(x: 3, y: 4) var moved = point // structs copy on assignment — "moved" is independent moved.y = 99 print(moved)
Haskell's point { y = 99 } is dedicated record-update syntax producing a new value directly. Swift has no equivalent update syntax for a plain struct — since structs are value types that copy on assignment, the idiomatic move is to copy into a fresh var and mutate that copy, relying on Swift's copy-on-assignment semantics to guarantee the original point is untouched.
Functions & Currying
Anonymous functions (closures)
main :: IO () main = print (map (\x -> x * x) [1, 2, 3, 4])
print([1, 2, 3, 4].map { x in x * x })
Haskell spells a lambda \x -> ... (the backslash evoking the Greek letter lambda). Swift spells the same idea as a closure, { x in ... }, and — when the parameter's type is already known from context — can shorten it further to the positional shorthand { $0 * $0 } seen in earlier rows.
Currying: automatic in Haskell, explicit in Swift
add :: Int -> Int -> Int add x y = x + y main :: IO () main = let addFive = add 5 in print (addFive 10)
func add(_ x: Int) -> (Int) -> Int { { y in x + y } } let addFive = add(5) print(addFive(10))
In Haskell, add 5 alone is already valid — every function is secretly a chain of one-argument functions. Swift removed its earlier special curried-function-declaration syntax (func add(_ x: Int)(_ y: Int) -> Int, valid through Swift 2) in favor of writing the currying out explicitly: a function that returns a closure. The result behaves identically to Haskell's automatic currying, but the intent must be spelled out by hand rather than falling out of the language's default calling convention.
Argument labels: a Swift feature with no Haskell parallel
-- Haskell function calls are purely positional — there is no -- concept of a labeled argument at all: greet :: String -> String -> String greet name greeting = greeting ++ ", " ++ name ++ "!" main :: IO () main = putStrLn (greet "Ada" "Hello")
func greet(name: String, greeting: String) -> String { "\(greeting), \(name)!" } print(greet(name: "Ada", greeting: "Hello"))
Swift call sites read almost like prose because argument labels are part of the function's identity and (by default) required at every call site — greet(name:greeting:). Haskell calls are purely positional; there is no labeled-argument concept at all, so getting equivalent call-site clarity in Haskell means choosing a very descriptive function name or a record argument instead.
Type Classes vs. Protocols
Type classes have a real, working Swift analogue
class Describable a where describe :: a -> String data Dog = Dog instance Describable Dog where describe _ = "a dog" main :: IO () main = putStrLn (describe Dog)
protocol Describable { func describe() -> String } struct Dog {} extension Dog: Describable { func describe() -> String { "a dog" } } print(Dog().describe())
This is a genuinely close match — closer than F#, Roc, or ReScript get. Haskell's class/instance and Swift's protocol/extension conformance solve the same problem the same way: declare required behavior once, then let each conforming type supply its own implementation, with the compiler dispatching to the right one automatically based on the value's type — no explicit passing of a dictionary of methods required in either language.
Default implementations via protocol extensions
import Data.Char (toUpper) class Greetable a where greeting :: a -> String loudGreeting :: a -> String loudGreeting x = map toUpper (greeting x) -- default method data Robot = Robot instance Greetable Robot where greeting _ = "beep boop" main :: IO () main = putStrLn (loudGreeting Robot)
protocol Greetable { func greeting() -> String } extension Greetable { func loudGreeting() -> String { greeting().uppercased() } } struct Robot: Greetable { func greeting() -> String { "beep boop" } } print(Robot().loudGreeting())
Haskell type classes may supply a default method implementation directly inside the class body, used automatically whenever an instance does not override it. Swift achieves the identical effect a different way: default implementations live in a separate extension on the protocol itself, rather than inside the protocol's own declaration (which may only list requirements, no bodies) — a structural difference, but the same "override only what differs" behavior for the conforming type.
deriving (Eq) versus protocol conformance
data Point = Point Int Int deriving (Eq, Show) main :: IO () main = print (Point 1 2 == Point 1 2)
struct Point: Equatable { let x: Int let y: Int } print(Point(x: 1, y: 2) == Point(x: 1, y: 2))
Both languages require an explicit opt-in for structural equality — Haskell's deriving (Eq) clause, Swift's : Equatable conformance — and both then synthesize the actual comparison automatically for a struct whose fields are themselves all Equatable, with no method body to write by hand in either case.
Either vs. throws
Either matches Result almost exactly
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)
struct DivisionError: Error { let message: String } func divide(_ x: Int, _ y: Int) -> Result<Int, DivisionError> { y == 0 ? .failure(DivisionError(message: "divide by zero")) : .success(x / y) } print(divide(10, 0))
Haskell's Either String Int and Swift's Result<Int, DivisionError> carry the same two cases in the same order (success type first) — only the case names differ: Haskell's Left/Right become Swift's .failure/.success. Swift additionally requires the failure type to conform to the Error protocol — a bare String does not qualify, unlike Haskell's Either, whose error type can be absolutely anything, including a plain String.
throws is Swift's OTHER error mechanism, with no Haskell equivalent
-- The closest Haskell equivalent to Swift's "throws" isn't Either -- at all — it's genuine runtime exceptions (Control.Exception), -- which idiomatic Haskell avoids in favor of Either/Maybe: 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)
enum MathError: Error { case divideByZero } func divide(_ x: Int, _ y: Int) throws -> Int { guard y != 0 else { throw MathError.divideByZero } return x / y } do { print(try divide(10, 0)) } catch { print("Error: \(error)") }
Unlike Haskell, which has exactly one idiomatic tool for expected failure (Either/Maybe), Swift genuinely has two co-equal, commonly-used mechanisms: Result (an ordinary value, as above) and throws/try/catch (propagated automatically by the language, closer to a checked exception). Neither is "the wrong one" in Swift — throws is often preferred for a function's primary failure mode, with Result reserved for passing a not-yet-handled outcome around as a value (e.g., in a completion handler).
Laziness vs. Strictness
No thunks: every binding is fully evaluated
main :: IO () main = do let expensive = 2 ^ 20 -- not computed until actually demanded putStrLn "before use" print expensive
import Foundation let expensive = pow(2.0, 20.0) // computed right here, immediately print("before use") print(expensive)
In Haskell, every binding is lazy unless forced otherwise — expensive is not actually computed until print demands it. Swift is strict by default, matching most mainstream languages: expensive is computed the moment its binding line runs.
lazy var: opt-in, not the default
main :: IO () main = do let expensive = 2 ^ 20 -- not computed until actually demanded putStrLn "before use" print expensive
import Foundation struct Computation { lazy var expensive: Double = { print("computing now") return pow(2.0, 20.0) }() } var computation = Computation() print("before use") print(computation.expensive) // "computing now" prints here, on first access
Achieving the same deferred, compute-once-and-cache behavior Haskell gives every binding for free requires Swift's explicit lazy keyword on a stored property (it has no equivalent for a plain local variable) — laziness is opt-in, per-property machinery in Swift, not the ambient evaluation model.
Garbage Collection vs. ARC
GC pauses versus deterministic reference counting
-- Haskell's GHC runtime uses a generational garbage collector — -- memory reclamation happens in occasional, unpredictable pauses -- the programmer does not directly control: main :: IO () main = print [1..1000]
final class Node { let value: Int init(value: Int) { self.value = value } deinit { print("deallocating \(value)") } } var node: Node? = Node(value: 42) node = nil // reference count drops to zero — deinit runs IMMEDIATELY, // deterministically, right here, not at some later GC pause
Haskell's GHC runtime reclaims memory with a generational garbage collector — reclamation happens in occasional pauses at a time the collector chooses, not the programmer. Swift uses Automatic Reference Counting (ARC): a class instance is deallocated the moment its reference count hits zero, deterministically and immediately, which is why deinit can be relied on to run at a precise, predictable point — a genuine capability Haskell's tracing garbage collector does not offer.
Gotchas for Haskell Developers
Classes are reference types — a real mutable-aliasing gotcha
-- Haskell has no reference-type/value-type distinction to worry -- about at all — every value is immutable and "shared" sharing -- is invisible, since nothing can ever mutate through an alias: main :: IO () main = print [1, 2, 3]
final class Counter { var value = 0 } let original = Counter() let alias = original // NOT a copy — "alias" and "original" are the // SAME object alias.value = 99 print(original.value) // prints 99, not 0 — a real gotcha for anyone // used to Haskell's value semantics everywhere
Haskell has nothing resembling reference-type aliasing to trip over — every value is immutable, so "sharing" a value is invisible and harmless. Swift class instances are reference types: assigning one to a new name does not copy it, so mutating through either name mutates the same underlying object, a genuine source of bugs for anyone arriving from a language where every value is immutable by default. (Swift structs, used throughout most of this page, do not have this problem — they are value types that copy on assignment.)
Force-unwrap (!) is an escape hatch back to Haskell-style crashes
main :: IO () main = print (Nothing :: Maybe Int)
let maybeValue: Int? = nil // The line below would crash at runtime with "Fatal error: Unexpectedly // found nil while unwrapping an Optional value" — Swift's "!" forces // an unwrap without checking, an explicit escape hatch back to the // kind of partial-function crash Haskell's "fromJust" also allows: // print(maybeValue!) print(maybeValue as Any)
Swift's optional type is meant to eliminate unchecked null access, but the ! force-unwrap operator is a deliberate, always-available escape hatch that crashes immediately if the value is actually nil — functionally identical to Haskell's fromJust, which is exactly as unsafe and exactly as tempting to reach for. Idiomatic style in both languages treats this operator/function as a last resort, not a first instinct.