Hello World & the Build
Hello, World
Look closely at the OCaml line and the Haskell one: they are almost the same declaration, and the type annotation Haskell writes out is the thing OCaml expresses with an empty tuple pattern.
let () = print_endline "Hello, World!"main :: IO ()
main = putStrLn "Hello, World!"OCaml's
let () = … binds the unit pattern, which both runs the expression and asserts it has type unit. Haskell declares a value named main of type IO () — an action that, when the runtime performs it, yields nothing useful. The distinction matters more than it looks: OCaml's line performs the printing as it is evaluated, while Haskell's main is a value describing a printing that the runtime carries out afterwards. The type signature is optional here — GHC would infer it — but writing top-level signatures is universal Haskell style, where OCaml leaves them to the .mli file.Formatted Output
Haskell has a
printf that looks identical and works by a completely different mechanism — one that gives up the compile-time checking OCaml has.let name = "OCaml"
let year = 1996
let () = Printf.printf "%s appeared in %d\n" name yearimport Text.Printf (printf)
main :: IO ()
main = do
let name = "Haskell"
year = 1990 :: Int
printf "%s appeared in %d\n" name yearOCaml's format string is a value of a built-in
format type, and the compiler reads its directives to determine exactly what arguments printf demands — a mismatch is a compile error. Haskell's printf is variadic through a type-class trick, and the format string is an ordinary String nobody inspects at compile time, so printf "%d" "text" compiles and fails at runtime. This is a rare case of OCaml having the stronger guarantee. Idiomatic Haskell mostly avoids printf in favor of putStrLn with show and string concatenation.dune and opam vs Cabal
Configuration rather than code, so neither column runs. The two ecosystems solve the same problem and disagree about where the version pinning lives.
(* dune-project *)
(lang dune 3.16)
(* bin/dune *)
(executable
(name main)
(libraries str))
(* Build and run:
dune build
dune exec bin/main.exe
Dependencies come from opam. *)-- demo.cabal
-- cabal-version: 2.4
-- name: demo
-- version: 0.1.0.0
--
-- executable demo
-- main-is: Main.hs
-- build-depends: base, containers
-- default-language: GHC2021
-- Build and run:
-- cabal build
-- cabal run
--
-- Dependencies come from Hackage; Stackage pins a working set.opam installs packages into a switch and dune compiles against whatever the active switch holds, which is why an OCaml build depends on machine state that the project files do not record. Cabal resolves dependencies per project and writes a plan, and Stack goes further by pinning an entire curated snapshot (Stackage) so that one line names a set of packages known to build together. The nearest OCaml equivalent is a lock file from
opam lock, which is opt-in and much less commonly used. The default-language: GHC2021 line has no OCaml counterpart at all — see the extensions row below.Language Extensions
A Haskell file may switch on language features one pragma at a time, and reading real Haskell means recognizing the common ones.
(* OCaml has one language. Syntax extensions exist as ppx
preprocessors declared in the build, never per file. *)
let describe pair =
match pair with
| (first, second) -> Printf.sprintf "%d and %d" first second
let () = print_endline (describe (3, 4)){-# LANGUAGE TupleSections #-}
{-# LANGUAGE LambdaCase #-}
describe :: (Int, Int) -> String
describe = \case
(first, second) -> show first ++ " and " ++ show second
main :: IO ()
main = putStrLn (describe (3, 4))There is no OCaml analogue: OCaml has a single language whose syntax is fixed, and its extension mechanism (ppx) is a build-time preprocessor configured in the dune file rather than a per-file pragma. GHC ships well over a hundred extensions, and a real module often enables a dozen. Modern practice is to adopt a bundle —
GHC2021 turns on the set considered settled — and add only the genuinely exotic ones per file. For someone reading Haskell for the first time, the pragmas at the top of a file are the fastest indication of how adventurous it is going to be.The Punctuation Swaps Round
:: and : Trade Places
The single most persistent source of typos when moving between these two languages, and it is worth meeting on the first page rather than the fiftieth.
(* :: conses. A type annotation uses a single colon. *)
let numbers : int list = 1 :: 2 :: [ 3 ]
let () = List.iter (Printf.printf "%d ") numbers; print_newline ()-- : conses. A type annotation uses a double colon.
numbers :: [Int]
numbers = 1 : 2 : [3]
main :: IO ()
main = mapM_ (\number -> putStr (show number ++ " ")) numbers >> putStrLn ""The two operators are exactly swapped. In OCaml,
:: is the list constructor and : introduces a type. In Haskell, : is the list constructor and :: introduces a type. Both readings are internally consistent and there is no way to remember which is which except by use. Note the list type too: OCaml writes int list with the constructor after its argument, Haskell writes [Int] with dedicated syntax, and for other constructors Haskell puts them first (Maybe Int where OCaml writes int option).Layout Replaces in
Haskell is indentation-sensitive, and its most characteristic binding form puts the helpers after the expression that uses them.
let describe values =
let count = List.length values in
let total = List.fold_left ( + ) 0 values in
Printf.sprintf "%d values, total %d" count total
let () = print_endline (describe [ 4; 8; 15 ])describe :: [Int] -> String
describe values = show count ++ " values, total " ++ show total
where
count = length values
total = sum values
main :: IO ()
main = putStrLn (describe [4, 8, 15])Haskell still has
let … in and it means what OCaml means by it. But the idiomatic form for function-local helpers is where, which attaches to the whole equation and is visible from every guard in it — something let cannot do. The effect on reading is real: an OCaml function builds up to its result from the top, while a Haskell function states its result first and explains the parts below. Neither is better; the reversal takes a while to stop feeling backwards.Multiple Equations per Function
Haskell lets a function be defined by several equations, each matching a different pattern in the argument position.
(* One equation, and the match is written out. *)
let rec length_of list =
match list with
| [] -> 0
| _ :: rest -> 1 + length_of rest
let () = Printf.printf "%d\n" (length_of [ 1; 2; 3 ])-- One equation per case, patterns in the head.
lengthOf :: [a] -> Int
lengthOf [] = 0
lengthOf (_ : rest) = 1 + lengthOf rest
main :: IO ()
main = print (lengthOf [1, 2, 3 :: Int])OCaml has one equation per function and does its case analysis inside a
match, with function as shorthand for the one-argument case. Haskell distributes the cases across separate equations tried in order, which reads well for a definition by recursion and less well when only one argument is being examined among several. Both check exhaustiveness — GHC needs -Wincomplete-patterns to warn, which is on by default in -Wall but not otherwise, so a missing case in Haskell can reach runtime as a "non-exhaustive patterns" crash where OCaml would have warned at compile time.Guards
Both languages have guards. Haskell writes them as a vertical bar per condition, attached to an equation rather than to a match case.
let classify number =
match number with
| 0 -> "zero"
| n when n < 0 -> "negative"
| n when n > 100 -> "huge"
| _ -> "ordinary"
let () =
List.iter
(fun number -> Printf.printf "%d is %s\n" number (classify number))
[ 0; -5; 500; 42 ]classify :: Int -> String
classify 0 = "zero"
classify number
| number < 0 = "negative"
| number > 100 = "huge"
| otherwise = "ordinary"
main :: IO ()
main = mapM_ (\number -> putStrLn (show number ++ " is " ++ classify number))
[0, -5, 500, 42]OCaml's
when clause guards one case of a match. Haskell's guards attach to an equation and are tried top to bottom, with otherwise — which is simply the value True under a more readable name — as the conventional catch-all. Neither language counts a guard toward exhaustiveness, so both columns need their fallback. Haskell guards can also bind, with pattern guards (| Just value <- lookup key table), which is the same expressive ground F#'s active patterns cover and which OCaml has no version of.Laziness
Nothing Is Computed Until Demanded
The defining difference between the two languages, and the one that changes how you are allowed to write everything else.
(* OCaml is strict: both branches' arguments are evaluated
before the function is called, so this DIVIDES BY ZERO. *)
let safe_divide numerator denominator fallback =
if denominator = 0 then fallback else numerator / denominator
let () =
(* 10 / 0 would raise, so the guard has to be in the caller. *)
let denominator = 0 in
let result = if denominator = 0 then -1 else safe_divide 10 denominator (-1) in
Printf.printf "%d\n" result-- Haskell is lazy: an argument is evaluated only if the
-- function actually demands it, so the division never happens.
safeDivide :: Int -> Int -> Int -> Int
safeDivide numerator denominator fallback =
if denominator == 0 then fallback else numerator `div` denominator
main :: IO ()
main = print (safeDivide 10 0 (-1))In OCaml every argument is evaluated before the call, so passing
10 / denominator to a function that might ignore it still divides. In Haskell an argument is a thunk — an unevaluated promise — forced only if and when the function demands its value, so the div above is never performed at all. This is why Haskell needs no special lazy keyword for a guard like this, and why OCaml programmers reach for lazy and Lazy.force, or wrap the value in a unit -> 'a thunk by hand. Note also the backticks: any Haskell function of two arguments can be written infix by surrounding its name with them.Infinite Data Structures
Because laziness is the default rather than an opt-in type, Haskell's ordinary list is already an infinite-capable structure.
(* An infinite LIST is impossible; OCaml needs the lazy
Seq type, which is a different type with its own module. *)
let () =
let naturals = Seq.ints 1 in
let squares = Seq.map (fun number -> number * number) naturals in
Seq.iter (Printf.printf "%d ") (Seq.take 5 squares);
print_newline ()-- The ordinary list type is already lazy, so an infinite
-- one needs nothing special and every list function works.
main :: IO ()
main = do
let squares = map (\number -> number * number) [1 ..]
mapM_ (\value -> putStr (show value ++ " ")) (take 5 squares)
putStrLn ""OCaml has to introduce a second sequence type,
Seq.t, with its own module of combinators, and code must choose between List and Seq up front — converting between them costs a traversal. Haskell has one list type that is lazy, so [1 ..] is a perfectly ordinary list, map and take are the ordinary functions, and the "streaming" version of an algorithm is usually just the algorithm. This is the most enjoyable consequence of laziness and the reason Haskell code composes as freely as it does.The Price: Space Leaks
The bill for laziness arrives as memory, and it arrives in the most ordinary operation there is.
(* A strict fold accumulates a number, and that is all
it ever holds. Memory is flat and obvious. *)
let () =
let total = List.fold_left ( + ) 0 (List.init 100000 (fun index -> index + 1)) in
Printf.printf "%d\n" totalimport Data.List (foldl')
-- foldl would build a 100,000-deep chain of unevaluated
-- additions before adding any of them. foldl' forces as it goes.
main :: IO ()
main = print (foldl' (+) 0 [1 .. 100000 :: Int])A lazy
foldl does not add as it goes: it builds a thunk representing ((0 + 1) + 2) + 3 … a hundred thousand levels deep, and only collapses it when the result is demanded — using far more memory than the numbers involved, and risking a stack overflow on the way down. foldl' (with the prime) forces each intermediate result, which is what OCaml's fold_left does unconditionally. The habit to build is to reach for foldl' by default, and to be suspicious of any accumulator that is not forced. Nothing in OCaml prepares you for this, because in a strict language the question cannot arise.Explicit Laziness in OCaml
OCaml can opt into exactly Haskell's evaluation strategy one value at a time, and putting the two side by side shows what "lazy by default" actually amounts to.
(* OCaml CAN be lazy on request: lazy builds a suspension,
Lazy.force evaluates it once and memoizes the result. *)
let () =
let expensive = lazy (21 * 2) in
print_endline "not yet";
Printf.printf "%d\n" (Lazy.force expensive);
Printf.printf "%d\n" (Lazy.force expensive)-- Every Haskell binding already IS that suspension, with the
-- same memoization. There is no keyword because there is no
-- choice to make.
main :: IO ()
main = do
let expensive = 21 * 2 :: Int
putStrLn "not yet"
print expensive
print expensiveThe two columns print the same three lines, and the whole lesson is in the code that produced them.
lazy (21 * 2) builds a suspension that is not evaluated at the binding; Lazy.force evaluates it the first time and returns the stored result every time after. That is precisely what a Haskell thunk is, which is why the Haskell column needs no annotation — an ordinary let already does it, and the multiplication happens at most once no matter how many times the value is printed. The difference is that OCaml makes it a type: int Lazy.t is not int, and the force is visible at every use. Haskell makes it the ordinary meaning of let and gives you seq and bang patterns as the opt-out.Purity & IO
Effects Show Up in the Type
The second defining difference. In Haskell an effect is not something a function does — it is something a function returns, and the type records it.
(* Any OCaml function may print, read a file or mutate.
The type says nothing about it. *)
let double value =
print_endline "(doubling)";
value * 2
let () = Printf.printf "%d\n" (double 21)-- A function that prints cannot have type Int -> Int. It
-- must be Int -> IO Int, and every caller inherits the IO.
double :: Int -> IO Int
double value = do
putStrLn "(doubling)"
return (value * 2)
main :: IO ()
main = do
result <- double 21
print resultOCaml's
double has type int -> int while printing to the terminal, and nothing in the signature warns a caller. Haskell will not allow that: printing produces a value of type IO (), so a function that prints has an IO in its result type, and that propagates to everything that calls it. The practical consequence is architectural rather than syntactic — Haskell pushes effects to the edges of a program because the type system makes their spread visible, and a large Haskell codebase ends up with a thin IO shell around a pure core. OCaml programmers often build the same architecture by discipline; Haskell makes it the path of least resistance.Sequencing Effects
A
do block reads like OCaml's semicolon sequencing, and the resemblance is close enough to be worth pinning down exactly.let () =
print_endline "first";
print_endline "second";
let total = 1 + 2 in
Printf.printf "%d\n" totalmain :: IO ()
main = do
putStrLn "first"
putStrLn "second"
let total = 1 + 2 :: Int
print totalOCaml's
; is an operator that evaluates the left side for its effect and then the right. Haskell's do is syntax that desugars into >>= and >> — so the block above is a single expression combining three IO actions into one, not a list of statements. The visible consequence is that let inside a do block has no in (it scopes to the rest of the block), and that binding a result from an action uses <- rather than let. Everything else about the shape carries over.Keeping the Core Pure
When a function genuinely computes rather than acts, the two languages produce almost the same code — and this is most of a program.
let summarize numbers =
let total = List.fold_left ( + ) 0 numbers in
let count = List.length numbers in
Printf.sprintf "%d values averaging %d" count (total / count)
let () = print_endline (summarize [ 4; 8; 15; 16 ])summarize :: [Int] -> String
summarize numbers = show count ++ " values averaging " ++ show (total `div` count)
where
total = sum numbers
count = length numbers
main :: IO ()
main = putStrLn (summarize [4, 8, 15, 16])Neither column touches
IO, because neither function does anything but compute. This is worth seeing after the previous two rows, because the impression they leave is that Haskell wraps everything in monads, and it does not: the majority of a Haskell program looks exactly like the majority of an OCaml program. What changes is that Haskell enforces the separation between this kind of function and the effectful kind, and OCaml leaves it to you. Note div for integer division, since / in Haskell is for fractional types only.Variables & Types
Type Inference and Signatures
Both languages infer everything. The cultural difference is where — and whether — the inferred type gets written down.
(* Signatures are optional and usually omitted in .ml;
they live in the .mli interface file instead. *)
let add first second = first + second
let () = Printf.printf "%d\n" (add 3 4)-- Signatures are optional and almost always written anyway,
-- immediately above the definition.
add :: Int -> Int -> Int
add first second = first + second
main :: IO ()
main = print (add 3 4)OCaml separates interface from implementation with a
.mli file, so a module's public signatures live in a different file from the code and there is nothing to write inline. Haskell has no separate interface file; the export list at the top of a module controls visibility, and type signatures sit directly above each definition. Writing them is not required but is near-universal, partly as documentation and partly because inference for a top-level definition can produce a more general or more surprising type than intended. There is also a real reason to write them: without one, the monomorphism restriction can make a binding less polymorphic than you expected.Numeric Polymorphism
The first appearance of type classes, in the place an OCaml programmer feels their absence most often.
(* OCaml has no overloading, so there are two functions:
one for int with *, one for float with *. *)
let double_int value = value * 2
let double_float value = value *. 2.0
let () =
Printf.printf "%d\n" (double_int 21);
Printf.printf "%.1f\n" (double_float 21.0)-- One function, constrained to any type that is a Num.
double :: Num a => a -> a
double value = value * 2
main :: IO ()
main = do
print (double (21 :: Int))
print (double (21.0 :: Double))OCaml has no operator overloading at all, which is why floats get their own family of operators (
+., *.) and why a numeric helper has to be written once per type. Haskell resolves * through the Num class, and Num a => a -> a reads as "for any type a that has a Num instance." The literal 2 is itself polymorphic — it means fromInteger 2 at whichever type is needed. What OCaml offers instead is a functor parameterized over the operations, which is more explicit, more verbose, and cannot be resolved automatically at the call site.Shadowing
This is one place the OCaml habit does not survive, and the reason is laziness rather than scoping.
let () =
let value = 5 in
let value = value * 2 in
let value = string_of_int value ^ " points" in
print_endline valuemain :: IO ()
main = do
let value = 5 :: Int
let doubled = value * 2
let labelled = show doubled ++ " points"
putStrLn labelledOCaml's
let value = value * 2 creates a new binding whose right-hand side refers to the old one, because the new name is not in scope until after the in. Haskell's let is recursive: the name being bound is in scope in its own right-hand side, so let value = value * 2 defines a value in terms of itself and produces an infinite loop rather than a doubling. GHC will warn about the shadowing with -Wall, but the program still hangs. Use a new name, as the target column does — this is a genuine trap, and it fails at runtime rather than at compile time.Tuples
Tuples and destructuring are the same, and so is the naming convention pressure that comes with them.
let divide_and_remainder numerator denominator =
(numerator / denominator, numerator mod denominator)
let () =
let (quotient, remainder) = divide_and_remainder 17 5 in
Printf.printf "%d remainder %d\n" quotient remainderdivideAndRemainder :: Int -> Int -> (Int, Int)
divideAndRemainder numerator denominator =
(numerator `div` denominator, numerator `mod` denominator)
main :: IO ()
main = do
let (quotient, remainder) = divideAndRemainder 17 5
putStrLn (show quotient ++ " remainder " ++ show remainder)The types are written the same way (
(Int, Int)) and destructured the same way. Haskell provides fst and snd for pairs and nothing for larger tuples, which is a gentle push toward records — the same push OCaml gives by not providing accessors either. The naming difference is conventional: Haskell uses camelCase for values and PascalCase for types and constructors, where OCaml uses snake_case for values and capitalizes modules and constructors.Strings
String Is a Linked List of Chars
Haskell's default string type is not an array of any kind, and the consequences run from mildly surprising to genuinely serious.
(* An OCaml string is a compact immutable byte array. *)
let () =
let text = "hello" in
Printf.printf "length = %d\n" (String.length text);
Printf.printf "first = %c\n" text.[0];
print_endline (text ^ " world")-- A Haskell String is literally [Char] — a linked list.
main :: IO ()
main = do
let text = "hello"
putStrLn ("length = " ++ show (length text))
putStrLn ("first = " ++ take 1 text)
putStrLn (text ++ " world")type String = [Char] is a real type synonym: a Haskell string is a lazy singly linked list of characters, one cons cell and one pointer per character. That makes length a full traversal, indexing linear, and memory use roughly an order of magnitude worse than OCaml's packed byte array. It also means every list function works on strings for free, which is why map, filter and reverse need no string-specific variants. For real text handling Haskell programs use Data.Text, a packed UTF-16 array much closer to what OCaml gives you by default — see the next row.Data.Text, the Real String Type
Serious Haskell text handling means a library type, a qualified import and a language pragma. This is a real ergonomic cost with no OCaml counterpart.
(* One string type, and it is already the efficient one. *)
let () =
let text = " padded " in
print_endline (String.trim text);
Printf.printf "%d\n" (String.length (String.trim text)){-# LANGUAGE OverloadedStrings #-}
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
main :: IO ()
main = do
let text = " padded " :: Text.Text
TextIO.putStrLn (Text.strip text)
print (Text.length (Text.strip text))Data.Text is a packed array of UTF-16 code units with the operations you would expect, and it is what any Haskell program handling real text uses. The friction is that it is not the type a string literal has, so OverloadedStrings is needed to let literals become Text, the module must be imported qualified because its names collide with the Prelude's list functions, and output needs Data.Text.IO rather than putStrLn. OCaml has one string type that is already packed, so none of this arises — though OCaml's is bytes with no encoding guarantee, where Text knows it holds Unicode.Converting To and From Strings
This is where type classes start paying visibly: one function name covers every type, including the ones you define.
(* One conversion function per type, named for both ends. *)
let () =
print_endline (string_of_int 42);
print_endline (string_of_float 3.5);
Printf.printf "%d\n" (int_of_string "42");
match int_of_string_opt "oops" with
| Some number -> Printf.printf "%d\n" number
| None -> print_endline "not a number"import Text.Read (readMaybe)
-- show and read are type-class methods, so they work for
-- every type with an instance — including your own.
main :: IO ()
main = do
putStrLn (show (42 :: Int))
putStrLn (show (3.5 :: Double))
print (read "42" :: Int)
case readMaybe "oops" :: Maybe Int of
Just number -> print number
Nothing -> putStrLn "not a number"OCaml names each conversion for both its source and destination (
string_of_int, int_of_string, float_of_string) and offers nothing at all for a user-defined type — printing a record means writing a printer or reaching for a ppx deriver. Haskell's show and read are methods of the Show and Read classes, and deriving (Show, Read) on a data type generates both. Note the annotations in the target column: since read is polymorphic in its result, the type has to come from somewhere, and here there is nothing else to determine it.Collections
Lists
Both are immutable singly linked lists. Note that the literal separator differs, and that Haskell's list is lazy where OCaml's is not.
let () =
let numbers = [ 1; 2; 3 ] in
let extended = 0 :: numbers in
List.iter (fun number -> Printf.printf "%d " number) extended;
print_newline ();
Printf.printf "length = %d\n" (List.length extended)main :: IO ()
main = do
let numbers = [1, 2, 3 :: Int]
extended = 0 : numbers
mapM_ (\number -> putStr (show number ++ " ")) extended
putStrLn ""
putStrLn ("length = " ++ show (length extended))OCaml separates list elements with semicolons and Haskell with commas — which is the more conventional choice, and is one more thing to retrain. The functions correspond closely:
List.map/map, List.filter/filter, List.length/length, List.rev/reverse, and Haskell's live in the Prelude rather than a module, so they need no qualification. Haskell adds range syntax ([1 .. 10]) and list comprehensions, neither of which OCaml has.List Comprehensions
A comprehension collapses nested iteration and filtering into one expression, and the difference in weight is not subtle.
(* No comprehension syntax: compose combinators. *)
let () =
let pairs =
List.concat_map
(fun first ->
List.filter_map
(fun second -> if first < second then Some (first, second) else None)
[ 1; 2; 3 ])
[ 1; 2; 3 ]
in
List.iter (fun (first, second) -> Printf.printf "(%d,%d) " first second) pairs;
print_newline ()main :: IO ()
main = do
let pairs = [ (first, second)
| first <- [1 .. 3 :: Int]
, second <- [1 .. 3]
, first < second ]
mapM_ (\(first, second) -> putStr ("(" ++ show first ++ "," ++ show second ++ ") ")) pairs
putStrLn ""The two columns produce the same pairs. Haskell's comprehension reads as set-builder notation: generators with
<-, filters as bare boolean expressions, and the result expression at the front. OCaml has to compose concat_map with filter_map, and every additional generator adds a nesting level. The comprehension is also lazy, so it can draw from an infinite generator as long as something downstream stops taking. This is one of the clearest ergonomic wins Haskell has over OCaml for everyday code.The Persistent Map
Both maps are persistent balanced trees. The difference is what supplies the ordering on the keys.
module StringMap = Map.Make (String)
let () =
let second = StringMap.add "language" "OCaml" StringMap.empty in
let third = StringMap.add "kind" "functional" second in
Printf.printf "second has %d\n" (StringMap.cardinal second);
Printf.printf "third has %d\n" (StringMap.cardinal third);
match StringMap.find_opt "language" third with
| Some value -> print_endline value
| None -> print_endline "absent"import qualified Data.Map as Map
main :: IO ()
main = do
let second = Map.insert "language" "Haskell" Map.empty
third = Map.insert "kind" "functional" second
putStrLn ("second has " ++ show (Map.size second))
putStrLn ("third has " ++ show (Map.size third))
case Map.lookup "language" third of
Just value -> putStrLn value
Nothing -> putStrLn "absent"OCaml's
Map.Make (String) is a functor application: it builds a module specialized to string keys, carrying the comparison function, and every operation goes through that module. Haskell's Map.insert has type Ord k => k -> a -> Map k a -> Map k a — the ordering comes from the Ord class and the compiler finds it, so there is nothing to instantiate. The qualified import is needed because Data.Map deliberately reuses Prelude names such as lookup, filter and map.Folding
The fold is the same operation with a different name and one important default.
let () =
let numbers = [ 4; 8; 15; 16; 23; 42 ] in
Printf.printf "sum = %d\n" (List.fold_left ( + ) 0 numbers);
Printf.printf "max = %d\n" (List.fold_left max min_int numbers);
Printf.printf "count = %d\n" (List.length numbers)import Data.List (foldl')
main :: IO ()
main = do
let numbers = [4, 8, 15, 16, 23, 42 :: Int]
putStrLn ("sum = " ++ show (foldl' (+) 0 numbers))
putStrLn ("max = " ++ show (maximum numbers))
putStrLn ("count = " ++ show (length numbers))OCaml's
fold_left is strict, and it is the right tool. Haskell's foldl is lazy and is almost never what you want — foldl' from Data.List is the strict version and the one to reach for, as the laziness section explained. Haskell's foldr corresponds to OCaml's fold_right but behaves very differently on long or infinite lists, since laziness lets it terminate early. Note also maximum, which raises on an empty list exactly as the OCaml column's min_int seed silently returns a wrong answer — neither is safe, and the safe versions are maximumMay-style helpers or a pattern match on the list.Functions & Composition
Currying and Partial Application
Currying is identical, including the way the type is written with arrows all the way down.
let add first second = first + second
let () =
let add_ten = add 10 in
Printf.printf "%d\n" (add_ten 5);
Printf.printf "%d\n" (add_ten 32)add :: Int -> Int -> Int
add first second = first + second
main :: IO ()
main = do
let addTen = add 10
print (addTen 5)
print (addTen 32)Both languages give
add the type Int -> Int -> Int, which associates to the right as Int -> (Int -> Int), so applying it to one argument is ordinary application rather than a special feature. This is knowledge that transfers with zero adjustment. Haskell leans on it harder in practice, because its standard library is uniformly designed for it and because operator sections — the subject of the next row — make partial application even lighter.Operator Sections
A section is a partially applied operator, and Haskell lets you fix either operand — which is what makes point-free style practical.
(* OCaml needs an explicit lambda, or the operator's
prefix form in parentheses. *)
let () =
let increment = ( + ) 1 in
let halve value = value / 2 in
List.iter (Printf.printf "%d ") (List.map increment [ 1; 2; 3 ]);
print_newline ();
List.iter (Printf.printf "%d ") (List.map halve [ 10; 20; 30 ]);
print_newline ()-- A section partially applies an operator on EITHER side.
main :: IO ()
main = do
mapM_ (\value -> putStr (show value ++ " ")) (map (+ 1) [1, 2, 3 :: Int])
putStrLn ""
mapM_ (\value -> putStr (show value ++ " ")) (map (`div` 2) [10, 20, 30 :: Int])
putStrLn ""Both languages can write
( + ) 1 to apply an operator prefix-style to its first argument. Only Haskell can write (`div` 2) to fix the second, which is the case that comes up constantly — dividing by, subtracting from, appending to. OCaml has to write a lambda. The one Haskell gotcha is (- 1), which parses as negative one rather than a subtraction section; subtract 1 is the workaround, and it is the single most common section mistake.Composition and Application
Both express a pipeline. OCaml threads a value left to right; Haskell composes functions right to left and applies at the end.
let () =
[ 1; 2; 3; 4; 5 ]
|> List.filter (fun number -> number mod 2 = 1)
|> List.map (fun number -> number * number)
|> List.fold_left ( + ) 0
|> Printf.printf "%d\n"import Data.List (foldl')
main :: IO ()
main = print
. foldl' (+) 0
. map (\number -> number * number)
. filter odd
$ [1, 2, 3, 4, 5 :: Int]OCaml's
|> is "apply the function on the right to the value on the left," so a pipeline reads in execution order. Haskell's . is genuine function composition — (f . g) x is f (g x) — so the pipeline reads in the opposite order, with $ at the end supplying the argument. Haskell does have & in Data.Function, which is exactly OCaml's |>, and some codebases prefer it. Being fluent in both directions is necessary to read Haskell, because the composition style is far more common.No Labeled or Optional Arguments
A genuine loss, and the same one F# has: OCaml's labeled and optional arguments are a language feature nobody else in the family copied.
let greet ?(greeting = "Hello") ~name () =
Printf.printf "%s, %s!\n" greeting name
let () =
greet ~name:"Ada" ();
greet ~greeting:"Welcome" ~name:"Alan" ()-- Haskell has neither. A record of options with defaults is
-- the usual substitute.
data Greeting = Greeting { greeting :: String, name :: String }
defaultGreeting :: Greeting
defaultGreeting = Greeting { greeting = "Hello", name = "" }
greet :: Greeting -> IO ()
greet options = putStrLn (greeting options ++ ", " ++ name options ++ "!")
main :: IO ()
main = do
greet defaultGreeting { name = "Ada" }
greet defaultGreeting { greeting = "Welcome", name = "Alan" }OCaml lets a parameter be named with
~ and defaulted with ?, and the trailing () marks the point at which the function is fully applied. Haskell has no such thing at any level, so the substitute is a record of options with a default value and record-update syntax to override fields — which is readable but requires declaring a type for what OCaml expresses in a signature. The upside is that the options record is a first-class value that can be built up and passed around, which the OCaml form cannot be.Records
Defining a Record
Records exist in both with functional update, but Haskell's field access reads backwards and the reason is worth knowing.
type point = { x : int; y : int }
let () =
let origin = { x = 0; y = 0 } in
let shifted = { origin with x = 5 } in
Printf.printf "(%d, %d)\n" origin.x origin.y;
Printf.printf "(%d, %d)\n" shifted.x shifted.ydata Point = Point { x :: Int, y :: Int }
main :: IO ()
main = do
let origin = Point { x = 0, y = 0 }
shifted = origin { x = 5 }
putStrLn ("(" ++ show (x origin) ++ ", " ++ show (y origin) ++ ")")
putStrLn ("(" ++ show (x shifted) ++ ", " ++ show (y shifted) ++ ")")OCaml writes
origin.x, and the field name is scoped to the record type. Haskell generates a top-level function called x of type Point -> Int, so access is x origin — a function call, not a projection. That is why Haskell records are so often criticized: two record types in one module cannot both have a field named x, because both would generate a top-level x. Modern GHC softens this with DuplicateRecordFields and OverloadedRecordDot (which finally gives origin.x), but a great deal of existing code prefixes every field with its type name to work around it. OCaml has never had this problem.Deriving Behavior
One clause generates a printer and a comparison. This is the everyday face of type classes and it is hard to give up once you have it.
(* Without ppx, printing and comparison are hand-written.
The polymorphic = works on any type, structurally. *)
type point = { x : int; y : int }
let to_string point = Printf.sprintf "{ x = %d; y = %d }" point.x point.y
let () =
print_endline (to_string { x = 1; y = 2 });
Printf.printf "%b\n" ({ x = 1; y = 2 } = { x = 1; y = 2 })data Point = Point { x :: Int, y :: Int }
deriving (Show, Eq)
main :: IO ()
main = do
print (Point { x = 1, y = 2 })
print (Point { x = 1, y = 2 } == Point { x = 1, y = 2 })deriving (Show, Eq) generates real instances at compile time, so print and == work on the type and — crucially — so does any generic code with a Show or Eq constraint. OCaml has no deriving in the language: ppx_deriving supplies it as a build-time preprocessor, spelled [@@deriving show, eq]. OCaml does have the polymorphic =, which compares any two values by walking their runtime representation, so structural equality needs nothing declared — at the cost of raising at runtime on functions and looping on cyclic values, both of which Haskell rejects at compile time.No Mutable Fields
OCaml lets a single field be mutable while the rest of the record stays immutable. Haskell has no such thing, and the workaround changes the function's type.
type counter = { mutable total : int }
let () =
let counter = { total = 0 } in
counter.total <- counter.total + 5;
counter.total <- counter.total + 5;
Printf.printf "%d\n" counter.totalimport Data.IORef
-- No mutable field exists. Mutation needs an IORef, and an
-- IORef can only be read or written inside IO.
data Counter = Counter { total :: IORef Int }
main :: IO ()
main = do
reference <- newIORef (0 :: Int)
let counter = Counter { total = reference }
modifyIORef' (total counter) (+ 5)
modifyIORef' (total counter) (+ 5)
value <- readIORef (total counter)
print valueOCaml's
mutable keyword makes one field assignable with <-, and the surrounding code stays ordinary. Haskell has no mutable fields at all: the field holds an IORef, which is a mutable box, and reading or writing it is an IO action — so purity is preserved and every function that touches the counter now advertises IO in its type. Note modifyIORef' with the prime, which forces the new value; the lazy modifyIORef accumulates thunks inside the reference and is a classic space leak.Data Types & Pattern Matching
Variants and Data Types
Sum types are the same feature. The difference is that Haskell constructors are curried functions rather than taking a tuple.
type shape =
| Circle of float
| Rectangle of float * float
| Point
let area = function
| Circle radius -> 3.14159 *. radius *. radius
| Rectangle (width, height) -> width *. height
| Point -> 0.0
let () =
List.iter
(fun shape -> Printf.printf "%.2f\n" (area shape))
[ Circle 1.0; Rectangle (2.0, 3.0); Point ]import Text.Printf (printf)
data Shape
= Circle Double
| Rectangle Double Double
| Point
area :: Shape -> Double
area (Circle radius) = 3.14159 * radius * radius
area (Rectangle width height) = width * height
area Point = 0.0
main :: IO ()
main = mapM_ (\shape -> printf "%.2f\n" (area shape))
[Circle 1.0, Rectangle 2.0 3.0, Point]OCaml's
Rectangle of float * float takes a tuple, so the constructor is applied as Rectangle (2.0, 3.0). Haskell's Rectangle Double Double is a curried function of two arguments, applied as Rectangle 2.0 3.0 — and it can be partially applied, so Rectangle 2.0 is a valid Double -> Shape. That is a genuine expressiveness difference, small but constantly visible. OCaml has an inline-record form (Rectangle of { width : float; height : float }) which Haskell matches with record syntax on a constructor.option and Maybe
The same type under two names, with two constructors that also differ only in spelling.
let () =
(match List.find_opt (fun number -> number mod 2 = 0) [ 1; 3; 4; 5 ] with
| Some number -> Printf.printf "found %d\n" number
| None -> print_endline "none found");
(match List.find_opt (fun number -> number mod 2 = 0) [ 1; 3; 5 ] with
| Some number -> Printf.printf "found %d\n" number
| None -> print_endline "none found")import Data.List (find)
describe :: [Int] -> IO ()
describe numbers =
case find even numbers of
Just number -> putStrLn ("found " ++ show number)
Nothing -> putStrLn "none found"
main :: IO ()
main = do
describe [1, 3, 4, 5]
describe [1, 3, 5]OCaml's
option with Some and None is Haskell's Maybe with Just and Nothing. The combinators correspond: Option.map is fmap, Option.bind is >>=, Option.value ~default is fromMaybe. The important structural difference is that Haskell's versions are not Maybe-specific — fmap and >>= are class methods that work for lists, Either, IO and everything else with an instance, which is the subject of the higher-kinded section below.newtype
Both wrap a type to keep it distinct. Haskell has a dedicated keyword with a guarantee the OCaml version does not make.
(* OCaml's equivalent is a single-constructor variant,
or an abstract type behind a signature. Both allocate
unless the compiler unboxes them. *)
type user_id = UserId of int
let describe (UserId value) = Printf.sprintf "user %d" value
let () = print_endline (describe (UserId 42))-- newtype is guaranteed zero-cost: it exists only at
-- compile time and is erased entirely.
newtype UserId = UserId Int
describe :: UserId -> String
describe (UserId value) = "user " ++ show value
main :: IO ()
main = putStrLn (describe (UserId 42))A Haskell
newtype may have exactly one constructor with exactly one field, and in exchange the compiler guarantees it is erased — a UserId is represented at runtime as an Int, with no allocation and no indirection. OCaml's single-constructor variant is a heap block unless the compiler can unbox it, which it does for some cases via [@@unboxed]. The other thing newtype buys is a second set of type-class instances for an existing type — the standard trick for giving one type two different Ords, which Haskell's coherence rule otherwise forbids.No Polymorphic Variants
One of the two features OCaml has that Haskell does not, and the one with no good substitute.
(* A polymorphic variant needs no declaration and can
belong to several types at once. *)
let describe value =
match value with
| `Circle radius -> Printf.sprintf "circle of %d" radius
| `Square side -> Printf.sprintf "square of %d" side
let () =
print_endline (describe (`Circle 3));
print_endline (describe (`Square 4))-- Haskell requires the data type to be declared, and every
-- constructor belongs to exactly one of them.
data Shape = Circle Int | Square Int
describe :: Shape -> String
describe (Circle radius) = "circle of " ++ show radius
describe (Square side) = "square of " ++ show side
main :: IO ()
main = do
putStrLn (describe (Circle 3))
putStrLn (describe (Square 4))A polymorphic variant tag belongs to no particular type: two unrelated functions can accept overlapping sets of tags, and the inferred type records exactly which tags a value may carry, giving open extensible sums with nothing declared in advance. Haskell has no equivalent in the language. The usual approximations are a type class with several instance types, which is open but abandons exhaustiveness checking, or an extensible-sum library built on type-level lists, which works and is heavy. The other OCaml-only feature is the module system itself, covered below.
Type Classes vs Modules & Functors
A Signature vs a Type Class
The central re-mapping of the page: an OCaml signature constrains a module, a Haskell class constrains a type.
module type Describable = sig
type t
val describe : t -> string
end
module IntDescription : Describable with type t = int = struct
type t = int
let describe value = Printf.sprintf "the number %d" value
end
let () = print_endline (IntDescription.describe 42)class Describable a where
describe :: a -> String
instance Describable Int where
describe value = "the number " ++ show value
main :: IO ()
main = putStrLn (describe (42 :: Int))The OCaml column has to name a module,
IntDescription, and the caller must know that name — the association between int and its description is carried by the module, not the type. Haskell attaches the instance to Int itself, so describe 42 resolves with nothing named at the call site, and any function with a Describable a constraint finds the instance automatically. That automatic resolution is what makes classes feel lighter, and it is bought with a rule OCaml does not impose — see the coherence row.A Functor vs a Constrained Function
The same abstraction written both ways. The size difference is the argument for type classes in one picture.
module type Comparable = sig
type t
val compare : t -> t -> int
end
module MakeLargest (Element : Comparable) = struct
let largest items =
List.fold_left
(fun best item -> if Element.compare item best > 0 then item else best)
(List.hd items) (List.tl items)
end
module IntCompare = struct
type t = int
let compare = compare
end
module LargestInt = MakeLargest (IntCompare)
let () = Printf.printf "%d\n" (LargestInt.largest [ 3; 9; 4 ])largest :: Ord a => [a] -> a
largest = foldr1 (\item best -> if item > best then item else best)
main :: IO ()
main = print (largest [3, 9, 4 :: Int])A functor is a function from modules to modules, so parameterizing means building a module, naming it, and calling through it. A class constraint (
Ord a =>) puts the requirement on the type variable and lets the compiler find the instance, so there is nothing to instantiate and nothing to name. Both are resolved at compile time and neither costs anything at runtime — GHC passes a dictionary but specializes it away in practice. What the functor can still do that the constraint cannot is take several types and several operations at once, and be applied twice to the same type with different behavior.Two Orderings for One Type
Here is what OCaml's modules buy that classes do not. Haskell enforces coherence: one type, one instance of a class, globally and forever.
(* A functor can be applied twice to the same type with
different behavior. Both modules coexist. *)
module Sorter (Order : sig type t val compare : t -> t -> int end) = struct
let sort items = List.sort Order.compare items
end
module Ascending = struct type t = int let compare = compare end
module Descending = struct type t = int let compare left right = compare right left end
module SortUp = Sorter (Ascending)
module SortDown = Sorter (Descending)
let () =
List.iter (Printf.printf "%d ") (SortUp.sort [ 3; 1; 2 ]);
print_newline ();
List.iter (Printf.printf "%d ") (SortDown.sort [ 3; 1; 2 ]);
print_newline ()import Data.List (sortBy)
import Data.Ord (comparing, Down(..))
-- A type has at most ONE Ord instance, globally. A second
-- ordering needs a newtype (Down) or a comparator value.
main :: IO ()
main = do
mapM_ (\value -> putStr (show value ++ " ")) (sortBy (comparing id) [3, 1, 2 :: Int])
putStrLn ""
mapM_ (\value -> putStr (show value ++ " ")) (sortBy (comparing Down) [3, 1, 2 :: Int])
putStrLn ""Coherence is what makes
describe 42 unambiguous — there is only one candidate, so the compiler never has to ask which, and two libraries can never disagree about how Int is ordered. The price is that Int cannot have two Ord instances, and OCaml's "apply the functor twice" has no translation. Haskell's two answers are both shown: pass a comparator as a value (sortBy), or wrap in a newtype whose only purpose is to carry the other instance — Down is exactly that, shipped in the standard library for this case.Hiding a Type
Both languages can make a type opaque to its users. OCaml does it with a signature, Haskell with an export list — and the difference in granularity is real.
(* A signature can hide a type's definition completely,
so callers cannot see that it is an int. *)
module Counter : sig
type t
val zero : t
val bump : t -> t
val to_int : t -> int
end = struct
type t = int
let zero = 0
let bump value = value + 1
let to_int value = value
end
let () =
let counter = Counter.bump (Counter.bump Counter.zero) in
Printf.printf "%d\n" (Counter.to_int counter)-- Haskell hides a type by exporting the type name without
-- its constructors. That is a module-level export list, so
-- it cannot be demonstrated inside one file — the export
-- would be: module Counter (Counter, zero, bump, toInt) where
newtype Counter = Counter Int
zero :: Counter
zero = Counter 0
bump :: Counter -> Counter
bump (Counter value) = Counter (value + 1)
toInt :: Counter -> Int
toInt (Counter value) = value
main :: IO ()
main = print (toInt (bump (bump zero)))OCaml's signature is a first-class thing: it can be named, reused, required by a functor, and applied to several implementations, and it hides the definition from everything outside the module including the rest of the same file. Haskell's mechanism is the module header's export list — exporting
Counter without Counter(..) hides the constructor — which is simpler, entirely adequate for this purpose, and not composable in the way a signature is. There is no Haskell way to say "this module must match that interface" and have the compiler check it independently of any use.Higher-Kinded Types
Abstracting Over a Type Constructor
This is the capability gap that most distinguishes the two type systems, and it is easy to miss because OCaml works around it so smoothly.
(* OCaml cannot write ONE map that works for option and
list: a type variable ranges over types, not over type
CONSTRUCTORS. So there are two functions. *)
let () =
let doubled_list = List.map (fun number -> number * 2) [ 1; 2; 3 ] in
let doubled_option = Option.map (fun number -> number * 2) (Some 21) in
List.iter (Printf.printf "%d ") doubled_list;
print_newline ();
match doubled_option with
| Some value -> Printf.printf "%d\n" value
| None -> print_endline "none"-- fmap is ONE function, for every type with a Functor
-- instance. The f in "Functor f" is a type constructor.
main :: IO ()
main = do
print (fmap (* 2) [1, 2, 3 :: Int])
print (fmap (* 2) (Just 21 :: Maybe Int))In OCaml a type variable stands for a type. In Haskell it may stand for a type constructor, so
Functor f quantifies over things like Maybe, [] and IO that are not types until applied to one. That is what makes a single fmap possible, and it is the foundation of Applicative, Monad, Traversable and the rest. OCaml can express this — by passing a module with a type 'a t, which is precisely what a functor does — but it cannot infer it, so every use is explicit and the abstraction stays local rather than becoming a shared vocabulary.One Vocabulary for Many Types
Because a class can range over type constructors, the same three or four operators serve every effect in the language.
(* Each container gets its own bind, with its own name,
from its own module. Nothing generalizes across them. *)
let () =
let from_option = Option.bind (Some 3) (fun value -> Some (value * 2)) in
let from_list = List.concat_map (fun value -> [ value; value * 2 ]) [ 1; 2 ] in
(match from_option with
| Some value -> Printf.printf "%d\n" value
| None -> print_endline "none");
List.iter (Printf.printf "%d ") from_list;
print_newline ()-- >>= is one operator, defined once per Monad instance.
main :: IO ()
main = do
print (Just 3 >>= \value -> Just (value * 2) :: Maybe Int)
print ([1, 2 :: Int] >>= \value -> [value, value * 2])OCaml gives each type its own
bind in its own module, and code written for one does not work for another. Haskell's >>= is a single method of Monad, so Maybe, lists, Either, IO, State and parser types all share it — and so does every function written generically over Monad m. That shared vocabulary is the real payoff of higher-kinded types, and the reason a Haskell programmer can reuse traverse, mapM and sequence across contexts that have nothing else in common. It is also the reason Haskell has a reputation for abstraction: the vocabulary has to be learned before ordinary library documentation reads clearly.do Notation vs let-operators
do vs let*
OCaml borrowed this idea from Haskell, and comparing them shows precisely what the higher-kinded machinery was for.
(* OCaml 4.08+ binding operators: define let* once and
the nesting flattens — but you define it per type. *)
let ( let* ) = Option.bind
let total first second =
let* left = int_of_string_opt first in
let* right = int_of_string_opt second in
Some (left + right)
let () =
(match total "3" "4" with
| Some value -> Printf.printf "%d\n" value
| None -> print_endline "not both numbers");
(match total "3" "oops" with
| Some value -> Printf.printf "%d\n" value
| None -> print_endline "not both numbers")import Text.Read (readMaybe)
-- do works for EVERY Monad with nothing defined first.
total :: String -> String -> Maybe Int
total first second = do
left <- readMaybe first
right <- readMaybe second
return (left + right)
main :: IO ()
main = do
print (total "3" "4")
print (total "3" "oops")OCaml's binding operators are a genuine improvement over nested matching, and the syntax is very close to
do. The difference is that let* must be defined for each type — the line let ( let* ) = Option.bind commits this scope to options, and using Result in the same scope means a different operator or a different scope. Haskell's do desugars to >>=, which is resolved by the Monad instance of whatever type the expression has, so the very same block works for Maybe, Either, lists and IO with nothing declared.Collecting Failures Across a List
The clearest demonstration of what a shared vocabulary is worth: a hand-written fold on one side, a single standard function on the other.
(* Written by hand: fold over the list, short-circuiting. *)
let parse_all texts =
List.fold_right
(fun text accumulated ->
match (int_of_string_opt text, accumulated) with
| Some number, Some rest -> Some (number :: rest)
| _ -> None)
texts (Some [])
let () =
(match parse_all [ "1"; "2"; "3" ] with
| Some numbers -> List.iter (Printf.printf "%d ") numbers; print_newline ()
| None -> print_endline "not all numbers");
(match parse_all [ "1"; "oops" ] with
| Some numbers -> List.iter (Printf.printf "%d ") numbers; print_newline ()
| None -> print_endline "not all numbers")import Text.Read (readMaybe)
-- traverse does exactly this, for every Traversable and
-- every Applicative, and it is one word.
main :: IO ()
main = do
print (traverse readMaybe ["1", "2", "3"] :: Maybe [Int])
print (traverse readMaybe ["1", "oops"] :: Maybe [Int])traverse turns a list of computations into a computation of a list, short-circuiting on the first failure — and because it is defined over Traversable t and Applicative f, the same function works for Maybe, Either, IO, and over trees and maps as well as lists. OCaml has no such function because it cannot abstract over the container and the effect at once; each combination is written out, and most codebases end up with a small pile of option_all, result_all and map_option helpers. This is worth knowing about before reading Haskell library code, where traverse is everywhere.Error Handling
result and Either
The same type again, with the arms named for their position rather than their meaning.
let parse_positive text =
match int_of_string_opt text with
| None -> Error (Printf.sprintf "%S is not a number" text)
| Some number when number <= 0 -> Error "must be positive"
| Some number -> Ok number
let () =
(match parse_positive "42" with
| Ok number -> Printf.printf "%d\n" number
| Error message -> print_endline message);
(match parse_positive "oops" with
| Ok number -> Printf.printf "%d\n" number
| Error message -> print_endline message)import Text.Read (readMaybe)
parsePositive :: String -> Either String Int
parsePositive text =
case readMaybe text of
Nothing -> Left (show text ++ " is not a number")
Just number
| number <= 0 -> Left "must be positive"
| otherwise -> Right number
main :: IO ()
main = do
print (parsePositive "42")
print (parsePositive "oops")OCaml's
('a, 'b) result has Ok and Error; Haskell's Either e a has Right and Left, with the convention that Right is the success (a pun on "right" meaning correct). Note that the type parameters are in the opposite order — the error comes first in Either and second in result — which is not arbitrary: it makes Either e a type constructor of one argument, so it can have Functor and Monad instances that operate on the success case. That is the higher-kinded machinery deciding the shape of the type.Exceptions
Both have exceptions. Haskell requires an instance declaration to define one and confines catching them to
IO.exception Too_large of int
let check value =
if value > 100 then raise (Too_large value) else value
let () =
Printf.printf "%d\n" (check 50);
(try Printf.printf "%d\n" (check 500) with
| Too_large value -> Printf.printf "too large: %d\n" value)import Control.Exception
data TooLarge = TooLarge Int deriving Show
instance Exception TooLarge
check :: Int -> IO Int
check value =
if value > 100 then throwIO (TooLarge value) else return value
main :: IO ()
main = do
first <- check 50
print first
result <- try (check 500) :: IO (Either TooLarge Int)
case result of
Left (TooLarge value) -> putStrLn ("too large: " ++ show value)
Right value -> print valueAn OCaml exception is a lightweight declaration, raisable and catchable anywhere, and idiomatic for ordinary control flow —
Not_found from a standard-library lookup is normal. A Haskell exception is a data type with an Exception instance, and while it can be thrown from pure code (via throw) it can only be caught in IO, because catching is an effect. That asymmetry pushes Haskell toward Either for anything a caller should handle and reserves exceptions for genuinely exceptional conditions and for IO failures. The try above needs its result type annotated so the handler knows which exception it is catching.Partial Functions
Both standard libraries ship functions that crash on the wrong input, and both communities regard them as a mistake — but the naming conventions differ.
(* Both hd and List.hd raise on an empty list; the safe
version is hd_opt, and the name says so. *)
let () =
Printf.printf "%d\n" (List.hd [ 1; 2; 3 ]);
match List.nth_opt [ 1; 2; 3 ] 10 with
| Some value -> Printf.printf "%d\n" value
| None -> print_endline "out of range"-- head and (!!) are partial and crash at runtime. The safe
-- forms are pattern matching and lookup-style functions.
describeFirst :: [Int] -> IO ()
describeFirst numbers =
case numbers of
(first : _) -> print first
[] -> putStrLn "empty"
main :: IO ()
main = do
describeFirst [1, 2, 3]
case drop 10 [1, 2, 3 :: Int] of
(value : _) -> print value
[] -> putStrLn "out of range"OCaml pairs each raising function with an
_opt variant (List.hd/List.hd_opt, List.nth/List.nth_opt), so the safe version is discoverable from the unsafe one. Haskell's head, tail, fromJust and !! have no such companions in the Prelude; the idiomatic answer is to pattern match instead, as above, or to use a library such as safe. GHC has recently begun warning on head and tail, which is a good indication of how the community feels about them.Mutation & State
ref vs IORef
The two mutable cells are nearly the same object. The difference is that every operation on Haskell's is an effect the type records.
let () =
let total = ref 0 in
List.iter (fun number -> total := !total + number) [ 1; 2; 3; 4 ];
Printf.printf "%d\n" !totalimport Data.IORef
main :: IO ()
main = do
total <- newIORef (0 :: Int)
mapM_ (\number -> modifyIORef' total (+ number)) [1, 2, 3, 4]
value <- readIORef total
print valueOCaml's
ref is a record with one mutable field, and ! and := are ordinary functions usable anywhere. Haskell's IORef is the same box, but newIORef, readIORef and modifyIORef all return IO actions, so a function that uses one has IO in its type and cannot be called from pure code. That is the purity rule applied consistently rather than a limitation of the box. Use modifyIORef' rather than modifyIORef: the unprimed version stores a thunk, and a loop like this one accumulates four thousand of them in a longer program.Local Mutation Without Losing Purity
Haskell has a mechanism for exactly the OCaml pattern above — mutate internally, expose a pure function — and it is checked rather than promised.
(* An OCaml function may mutate internally and still
present a pure interface. Nothing enforces or records
the distinction — it is a convention. *)
let sum_to limit =
let total = ref 0 in
for index = 1 to limit do
total := !total + index
done;
!total
let () = Printf.printf "%d\n" (sum_to 100)import Control.Monad.ST
import Data.STRef
-- ST allows real mutation, and runST proves the mutation
-- cannot escape — so the function's type is genuinely pure.
sumTo :: Int -> Int
sumTo limit = runST $ do
total <- newSTRef 0
mapM_ (\index -> modifySTRef' total (+ index)) [1 .. limit]
readSTRef total
main :: IO ()
main = print (sumTo 100)The OCaml function's type is
int -> int whether or not it mutates, so its purity is a claim the reader has to take on trust. runST makes the same claim checkable: the ST monad carries a phantom type variable that runST quantifies over in a way that makes any escaping reference a type error, so a function using it really is pure and the compiler knows it. This is the piece of Haskell that answers "but sometimes mutation is the right algorithm" — you get the mutable array or counter, and you still get the pure signature.Concurrency
Spawning Work
Both spawn concurrent work, and the unit being spawned is a different order of magnitude in each.
(* OCaml 5's Domain is an OS thread with its own minor
heap. They are expensive; you create a few. *)
let () =
let worker = Domain.spawn (fun () ->
let total = ref 0 in
for index = 1 to 100 do total := !total + index done;
!total)
in
Printf.printf "%d\n" (Domain.join worker)import Control.Concurrent
import Control.Concurrent.MVar
-- forkIO makes a GREEN thread. They cost a few hundred
-- bytes; creating a million is ordinary.
main :: IO ()
main = do
result <- newEmptyMVar
_ <- forkIO (putMVar result (sum [1 .. 100 :: Int]))
value <- takeMVar result
print valueAn OCaml
Domain is an operating-system thread with its own minor heap, so the guidance is to create roughly as many as you have cores and multiplex work across them with a library scheduler. A Haskell forkIO thread is a green thread managed by the GHC runtime, costing a few hundred bytes, so spawning one per connection or per task is normal practice. Haskell also has real parallelism across cores — the runtime multiplexes green threads onto OS threads when built with -threaded. An MVar is a mutable box that is either full or empty, so it serves as both a mutex and a one-shot channel, which is why it works as the result slot here.Shared Mutable State
Both reach 2000. The difference is whether forgetting the lock is possible.
let () =
let lock = Mutex.create () in
let total = ref 0 in
let bump () =
for _ = 1 to 1000 do
Mutex.lock lock;
total := !total + 1;
Mutex.unlock lock
done
in
let first = Domain.spawn bump in
let second = Domain.spawn bump in
Domain.join first;
Domain.join second;
Printf.printf "%d\n" !totalimport Control.Concurrent
import Control.Concurrent.MVar
main :: IO ()
main = do
total <- newMVar (0 :: Int)
done <- newEmptyMVar
let bump = do
mapM_ (\_ -> modifyMVar_ total (return . (+ 1))) [1 .. 1000 :: Int]
putMVar done ()
_ <- forkIO bump
_ <- forkIO bump
takeMVar done
takeMVar done
value <- readMVar total
print valueIn OCaml the mutex and the data are separate, and the connection between them exists only in the programmer's head — delete the
Mutex.lock lines and the program still compiles and quietly loses increments. An MVar puts the data inside the lock: taking it empties the box, so no other thread can proceed until it is put back, and modifyMVar_ handles both halves. Forgetting is not expressible. OCaml 5's runtime does guarantee that a data race cannot corrupt memory, so the failure mode is a wrong number rather than a crash — but a wrong number is still wrong.Software Transactional Memory
Haskell's standard library has software transactional memory, and it is the strongest concurrency feature either language offers.
(* OCaml has no STM in the standard library. Composing
two locked operations atomically means taking both locks
in a fixed order and getting it right by hand. *)
let () =
let lock = Mutex.create () in
let first_account = ref 100 in
let second_account = ref 0 in
Mutex.lock lock;
first_account := !first_account - 30;
second_account := !second_account + 30;
Mutex.unlock lock;
Printf.printf "%d %d\n" !first_account !second_accountimport Control.Concurrent.STM
-- Two updates compose into ONE atomic transaction, with no
-- lock ordering to get right and no deadlock to design around.
main :: IO ()
main = do
firstAccount <- newTVarIO (100 :: Int)
secondAccount <- newTVarIO (0 :: Int)
atomically $ do
modifyTVar' firstAccount (subtract 30)
modifyTVar' secondAccount (+ 30)
first <- readTVarIO firstAccount
second <- readTVarIO secondAccount
putStrLn (show first ++ " " ++ show second)An
STM action is a transaction: it runs optimistically, and if another thread touched anything it read, it is retried. The property that matters is composability — two correct transactions combine into one correct transaction with no lock ordering to design and no possibility of deadlock, which is exactly what mutexes cannot do. The type system enforces it: STM is a separate monad, so no IO can leak into a transaction that might be retried. OCaml has no equivalent in the standard library; the kcas package supplies one, and it is a library choice rather than the default.