PONY λ M2 Modula-2

Haskell.CodeCompared.To/OCaml

An interactive executable cheatsheet comparing Haskell and OCaml

GHC 9.12 OCaml 5.2
Output & Basics
Hello, World
main :: IO () main = putStrLn "Hello, World!"
let () = print_endline "Hello, World!"
Haskell needs a named main :: IO () entry point that the runtime calls once. OCaml has no single required entry point at all — a source file is a sequence of top-level bindings evaluated in order, and let () = ... is simply a binding whose left side is the unit pattern, run purely for its side effect. print_endline is the direct analogue of putStrLn.
Sequencing multiple outputs
main :: IO () main = do putStrLn "First" putStrLn "Second" putStrLn "Third"
let () = print_endline "First"; print_endline "Second"; print_endline "Third"
Haskell needs do-notation to sequence IO actions, since it is sugar over chained monadic binds. OCaml is strict, so it needs no monad for this at all: the semicolon ; simply evaluates the left expression (which must have type unit), discards the result, and evaluates the right expression next, in the order written.
Formatted output
import Text.Printf (printf) main :: IO () main = printf "%s is %d years old\n" "Alice" (30 :: Int)
let () = Printf.printf "%s is %d years old\n" "Alice" 30
Both languages check the format string against the argument types at compile time — a rare guarantee most languages' printf-style functions lack entirely. Haskell achieves it through Text.Printf's typeclass-based variadic trick; OCaml achieves it because the compiler gives Printf.printf's format string its own special-cased type, format, parsed directly by the type checker.
Values & Bindings
Immutable bindings look nearly identical
main :: IO () main = let number = 42 in print number
let number = 42 let () = Printf.printf "%d\n" number
Both languages bind names with let, and in both, that binding cannot be reassigned — number = 99 afterward is a compile error in either language. This is one of the strongest syntactic and semantic overlaps between the two: the ML-family let is shared, not merely similar.
Real mutation, walled off differently
import Data.IORef main :: IO () main = do counter <- newIORef (0 :: Int) modifyIORef counter (+ 1) value <- readIORef counter print value
let counter = ref 0 let () = counter := !counter + 1; Printf.printf "%d\n" !counter
Both languages make mutation explicit and opt-in rather than the default — but they wall it off differently. Haskell requires an IORef to live inside the IO monad, so the type signature IO Int announces "this may mutate" before you even read the body. OCaml's ref needs no such monadic context at all: ref 0 allocates a mutable cell anywhere, ! dereferences it, and := assigns it, all as ordinary expressions — mutation is real and unrestricted, just not the default the way it would be in Ruby.
Shadowing a name is legal in both
main :: IO () main = let value = 5 value' = show value in putStrLn value'
let value = 5 let value = string_of_int value let () = print_endline value
Both languages let a new let introduce a fresh binding that shadows an older one with the same name, without being an error — each is a distinct binding under the hood, not a mutation of the first. Haskell idiom favors a trailing prime (value') to keep them visually distinct; OCaml programmers commonly just reuse the bare name, since the shadowing is unambiguous to the compiler either way.
Types
Both infer types via Hindley-Milner
addOne :: Num a => a -> a addOne number = number + 1 main :: IO () main = print (addOne 5)
let add_one number = number + 1 let () = Printf.printf "%d\n" (add_one 5)
Neither function needed a type annotation — both compilers inferred the full type from how number is used. This is the single most important thing the two languages share: a Hindley-Milner-family inference engine that can reconstruct an entire function's type from its body, with annotations serving as documentation and compiler-checked sanity checks rather than a requirement.
Polymorphic number literals versus one concrete type each
describeSum :: Double describeSum = 2 + 3.5 main :: IO () main = print describeSum
(* OCaml: + is ONLY for int; +. is the separate operator for float. No overloading exists. *) let describe_sum = 2.0 +. 3.5 let () = Printf.printf "%g\n" describe_sum
This is the sharpest type-system gotcha for a Haskeller arriving from the same ML family. In Haskell, numeric literals are polymorphic through the Num typeclass, so 2 + 3.5 just works: 2 is inferred as a Double to match. OCaml has no operator overloading at all+ works only on int, and a completely separate operator, +., works only on float. Writing 2 + 3.5 in OCaml is a compile error; the literal 2 must be written 2.0 and combined with +..
Type annotations are documentation, not a requirement
square :: Int -> Int square number = number * number main :: IO () main = print (square 6)
let square (number : int) : int = number * number let () = Printf.printf "%d\n" (square 6)
Both languages let you annotate a function's parameter and return types, and both will infer them perfectly well without any annotation at all. The syntax differs — Haskell's separate :: signature line above the definition versus OCaml's inline (name : type) parameter annotations — but the underlying philosophy, "annotate for the reader, not for the compiler," is identical.
Strings
Concatenation: `++` versus `^`
main :: IO () main = putStrLn ("Hello, " ++ "World!")
let () = print_endline ("Hello, " ^ "World!")
Both languages use a dedicated infix operator for string concatenation rather than overloading +, but they picked different symbols: Haskell's ++ is the same operator used to concatenate any list (since a String is [Char]); OCaml gives strings their own dedicated operator, ^, distinct from its list-concatenation operator @, because an OCaml string is not a list under the hood.
Strings are immutable in both, by default
main :: IO () main = let greeting = "hello" in putStrLn (map (\c -> if c == 'h' then 'H' else c) greeting)
let greeting = "hello" let () = print_endline (String.map (fun character -> if character = 'h' then 'H' else character) greeting)
Both languages treat strings as immutable values by default, and both build a transformed copy rather than mutating in place — Haskell's map over a String (really a [Char]) and OCaml's String.map do the identical job. OCaml does still expose a genuinely mutable Bytes.t type for the rare case in-place mutation is needed, with no Haskell equivalent at all outside of the ST or IO monads.
Length and indexing
main :: IO () main = let word = "hello" in print (length word, word !! 1)
let word = "hello" let () = Printf.printf "%d %c\n" (String.length word) word.[1]
Haskell's length and !! work on any list, strings included, and both are O(n) since a Haskell String is a genuine linked list of characters. OCaml strings are packed byte arrays, so String.length and the special .[index] syntax are both O(1) — a real performance difference that follows directly from the two languages' different underlying string representations.
Collections
List literals match; the cons operator does not
main :: IO () main = print (1 : [2, 3, 4, 5])
let () = let numbers = 1 :: [2; 3; 4; 5] in List.iter (fun number -> Printf.printf "%d " number) numbers
The bracketed list literal looks almost the same, but two symbols swap meaning between the languages: Haskell's cons operator is : and its list-literal separator is ,; OCaml's cons operator is :: and its list-literal separator is ;. Reaching for a comma inside an OCaml list literal ([2, 3, 4]) is a classic Haskeller mistake — it does not error the way you'd expect, since OCaml would parse [2, 3, 4] as a single-element list containing a tuple.
OCaml arrays are genuinely mutable and fixed-size
import Data.Array main :: IO () main = let numbers = listArray (0, 4) [1, 2, 3, 4, 5] updated = numbers // [(2, 99)] in print (elems updated)
let numbers = [| 1; 2; 3; 4; 5 |] let () = numbers.(2) <- 99; Array.iter (fun number -> Printf.printf "%d " number) numbers
This is a genuine capability gap, not just a syntax difference. Haskell's Data.Array is immutable — numbers // [(2, 99)] builds an entirely new array, leaving the original untouched (mutable arrays exist only via IOArray/STArray inside IO/ST). OCaml's array is mutable by default and in placenumbers.(2) <- 99 updates the existing array directly, with no monad, no wrapper type, and no new allocation, because OCaml never tried to make mutation the exceptional case in the first place.
map, filter, and fold read almost the same
main :: IO () main = print (foldr (+) 0 (filter even (map (* 2) [1, 2, 3, 4, 5])))
let () = let result = [1; 2; 3; 4; 5] |> List.map (fun number -> number * 2) |> List.filter (fun number -> number mod 2 = 0) |> List.fold_left (+) 0 in Printf.printf "%d\n" result
The three functions have direct one-to-one counterparts — List.map, List.filter, List.fold_left/List.fold_right — and behave the same way. The idiomatic style of chaining them differs: Haskell tends to nest function calls inside-out or use . composition, while OCaml's own |> pipe operator (which later inspired F#'s and Elixir's identical operator) lets the pipeline read top-to-bottom in the order it executes.
Recursing over a list with the cons pattern
sumList :: [Int] -> Int sumList [] = 0 sumList (x : xs) = x + sumList xs main :: IO () main = print (sumList [1, 2, 3, 4])
let rec sum_list = function | [] -> 0 | head :: tail -> head + sum_list tail let () = Printf.printf "%d\n" (sum_list [1; 2; 3; 4])
The recursive structure and the cons pattern itself are identical in spirit — split into empty-list and head/tail cases. Notice the rec keyword on the OCaml side: unlike Haskell, where every top-level binding can refer to itself and its siblings automatically, OCaml requires let rec (rather than plain let) whenever a function needs to call itself.
Control Flow
`if` is an expression in both languages
main :: IO () main = let age = 20 in putStrLn (if age >= 18 then "adult" else "minor")
let age = 20 let () = print_endline (if age >= 18 then "adult" else "minor")
Both languages treat if as an expression that produces a value, not a statement — both branches are mandatory, and both must have the same type, because the whole construct must evaluate to one value. This is a place where OCaml and Haskell agree with each other and diverge from most C-descended languages, where if is a statement and a separate ternary operator is needed for the expression form.
OCaml has real `for` loops; Haskell has none
import Control.Monad (forM_) main :: IO () main = forM_ [1 .. 5] print
let () = for number = 1 to 5 do Printf.printf "%d\n" number done
This difference follows directly from strictness. OCaml has a genuine imperative for ... to ... do ... done loop construct built into the language, because iterating and mutating are ordinary, unremarkable operations in a strict language. Haskell has no loop construct of any kind — forM_ is an ordinary library function (not special syntax) that repeats an IO action once per list element, the closest Haskell gets to "looping".
`while` loops, for the same reason
import Data.IORef import Control.Monad (when) main :: IO () main = do counter <- newIORef (0 :: Int) let loop = do value <- readIORef counter when (value < 3) $ do print value modifyIORef counter (+ 1) loop loop
let () = let counter = ref 0 in while !counter < 3 do Printf.printf "%d\n" !counter; incr counter done
OCaml's built-in while ... do ... done reads exactly like the imperative loop a Rubyist or a C programmer already knows, because mutable state and repeated mutation are ordinary in a strict language. Haskell has to build the equivalent by hand out of an IORef and explicit recursion — there is no dedicated loop syntax to reach for at all, since looping-via-mutation is exactly the pattern Haskell's design steers you away from.
Pattern Matching
`match ... with` and `case ... of` are the same idea
describe :: Int -> String describe number = case number of 0 -> "zero" 1 -> "one" _ -> "many" main :: IO () main = putStrLn (describe 1)
let describe number = match number with | 0 -> "zero" | 1 -> "one" | _ -> "many" let () = print_endline (describe 1)
Structurally these are the same construct with different keywords: case number of versus match number with, both followed by a list of patterns and results, both defaulting to _ as the catch-all. Both compilers also share the same valuable guarantee: an incomplete match triggers a compiler warning (and, in OCaml, can be promoted to a hard error), rather than silently doing nothing.
Guards inside a match arm
classify :: Int -> String classify n | n < 0 = "negative" | n == 0 = "zero" | otherwise = "positive" main :: IO () main = putStrLn (classify (-5))
let classify number = match number with | n when n < 0 -> "negative" | 0 -> "zero" | _ -> "positive" let () = print_endline (classify (-5))
Haskell's pipe-guard syntax on an equation and OCaml's when clause on a match arm do the identical job: attaching an extra boolean condition to a pattern that must also hold for that branch to be chosen. OCaml's when guard is written per-arm inside one match, where Haskell's guards live directly on the function equation instead of inside an explicit case.
The wildcard and "as"-binding both transfer
describeList :: [Int] -> String describeList whole@(first : _) = show first ++ " starts " ++ show whole describeList [] = "empty" main :: IO () main = putStrLn (describeList [1, 2, 3])
let describe_list = function | (first :: _) as whole -> Printf.sprintf "%d starts [%s]" first (String.concat "; " (List.map string_of_int whole)) | [] -> "empty" let () = print_endline (describe_list [1; 2; 3])
The underscore wildcard _ means the same thing in both. The "as"-pattern also transfers, just with the keyword on opposite sides: Haskell writes whole@(first : _) with the binding name first; OCaml writes (first :: _) as whole with the binding name last — same capability (bind both the whole value and a piece of its structure in one pattern), mirrored syntax.
Functions
Recursion is automatic in Haskell, explicit in OCaml
factorial :: Integer -> Integer factorial 0 = 1 factorial n = n * factorial (n - 1) main :: IO () main = print (factorial 5)
let rec factorial n = if n = 0 then 1 else n * factorial (n - 1) let () = Printf.printf "%d\n" (factorial 5)
A genuine, easy-to-forget gotcha: every Haskell binding, top-level or let, can refer to itself (and its siblings) with no special keyword — recursion is simply the default. OCaml requires the explicit rec keyword: plain let factorial n = ... factorial (n - 1) ... without rec either fails to compile (the inner factorial is unbound) or, worse, silently refers to some unrelated outer binding of the same name if one exists.
Currying and partial application, in both, for free
addThree :: Int -> Int -> Int -> Int addThree x y z = x + y + z main :: IO () main = let addOneAndTwo = addThree 1 2 in print (addOneAndTwo 3)
let add_three x y z = x + y + z let () = let add_one_and_two = add_three 1 2 in Printf.printf "%d\n" (add_one_and_two 3)
Every function in both languages is curried automatically — a three-argument function is really a chain of three one-argument functions, and supplying fewer arguments than the full arity yields a new function waiting for the rest, with no special closure syntax needed in either language. This is a deep shared trait of the ML lineage, not a coincidence.
The pipe operator was born in OCaml
import Data.Function ((&)) main :: IO () main = print ([1, 2, 3, 4, 5] & filter even & map (* 2) & sum)
let () = [1; 2; 3; 4; 5] |> List.filter (fun number -> number mod 2 = 0) |> List.map (fun number -> number * 2) |> List.fold_left (+) 0 |> Printf.printf "%d\n"
OCaml's |> is where the pipe operator was born — F#, Elixir, and others all copied it directly from OCaml, and it is idiomatic, everyday OCaml. Haskell has no built-in equivalent; its most common combinator, $, runs the opposite direction (low-precedence right-to-left application, avoiding parentheses rather than sequencing a pipeline), and the left-to-right pipe some Haskellers reach for, & from Data.Function, is a small library addition rather than a core, ubiquitous idiom.
Option Type
`Maybe` and `option` are the same idea, different names
safeDivide :: Int -> Int -> Maybe Int safeDivide _ 0 = Nothing safeDivide x y = Just (x `div` y) main :: IO () main = print (safeDivide 10 2, safeDivide 10 0)
let safe_divide x y = if y = 0 then None else Some (x / y) let () = match safe_divide 10 2, safe_divide 10 0 with | Some a, None -> Printf.printf "%d None\n" a | _ -> print_endline "unexpected"
This is a near-perfect one-to-one correspondence: Haskell's Just/Nothing and OCaml's Some/None are the identical type in every way that matters, both making "this might not have a value" a type-level fact the compiler forces you to handle, rather than a runtime nil that can silently propagate.
Pattern matching an option value
describe :: Maybe Int -> String describe Nothing = "nothing here" describe (Just n) = "got " ++ show n main :: IO () main = putStrLn (describe (Just 42))
let describe = function | None -> "nothing here" | Some n -> "got " ^ string_of_int n let () = print_endline (describe (Some 42))
Both languages force you to handle both cases through ordinary pattern matching — there is no way to reach for the wrapped value without acknowledging the empty case might occur instead, in either language. The syntax is nearly a character-for-character match once you swap Nothing/Just for None/Some.
Transforming the value inside, if present
main :: IO () main = print (fmap (* 2) (Just 21))
let () = match Option.map (fun number -> number * 2) (Some 21) with | Some result -> Printf.printf "%d\n" result | None -> print_endline "None"
Haskell's fmap comes from the general Functor typeclass, so the identical call also works on lists, Either, and any other functor — one function, many types. OCaml has no typeclass mechanism to unify this, so each module provides its own separately named function: Option.map for options, List.map for lists, and so on — same behavior for options specifically, but no shared abstraction tying them together.
Variants vs. Algebraic Data Types
`type ... = A | B of ...` mirrors `data`
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 (area (Circle 2.0))
type shape = | Circle of float | Rectangle of float * float let area = function | Circle radius -> Float.pi *. radius *. radius | Rectangle (width, height) -> width *. height let () = Printf.printf "%g\n" (area (Circle 2.0))
This is nearly a direct syntax transliteration: Haskell's data Shape = Circle Double | Rectangle Double Double and OCaml's type shape = Circle of float | Rectangle of float * float declare the identical sum type, with capitalized constructor names in both languages and exhaustive pattern matching enforced by both compilers.
A recursive variant: a binary tree
data Tree = Leaf | Node Tree Int Tree sumTree :: Tree -> Int sumTree Leaf = 0 sumTree (Node l v r) = sumTree l + v + sumTree r 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 sum_tree = function | Leaf -> 0 | Node (left, value, right) -> sum_tree left + value + sum_tree right let () = Printf.printf "%d\n" (sum_tree (Node (Node (Leaf, 1, Leaf), 2, Node (Leaf, 3, Leaf))))
A recursive algebraic data type — here a binary tree referencing itself in its own definition — works identically in both languages, and the recursive traversal function has the same two-case shape: a base case for the empty constructor, a recursive case that processes both children. This pattern (self-referential sum types plus structurally recursive functions over them) is the bread and butter of both languages equally.
Polymorphic variants have no Haskell equivalent
-- Haskell has no direct equivalent: constructors always belong to one -- specific, pre-declared type, never inferred ad hoc from usage alone. data Color = Red | Green | Blue describe :: Color -> String describe Red = "red" describe _ = "other" main :: IO () main = putStrLn (describe Red)
(* OCaml: a polymorphic variant needs no prior type declaration at all — the backtick-tagged constructor is simply inferred from how it's used. *) let describe = function | `Red -> "red" | _ -> "other" let () = print_endline (describe `Red)
Polymorphic variants (the backtick-prefixed constructors, like \`Red) are a genuinely OCaml-specific feature with no Haskell counterpart. An ordinary OCaml variant, like an ordinary Haskell data type, must be declared before use and is a fixed, closed set of constructors. A polymorphic variant needs no declaration at all — the type is inferred structurally from the constructors actually used, and different functions can accept overlapping or open-ended sets of tags. This is a form of structural typing Haskell's nominal type system does not offer.
Records
Record declaration and construction look alike
data Person = Person { name :: String, age :: Int } main :: IO () main = let person = Person { name = "Alice", age = 30 } in putStrLn (name person ++ " is " ++ show (age person))
type person = { name : string; age : int } let () = let person = { name = "Alice"; age = 30 } in Printf.printf "%s is %d\n" person.name person.age
Record declaration, construction, and field access are all close cousins: named fields with types, construction with { field = value; ... }, and access with either a generated field-accessor function (Haskell's name person) or dot notation (OCaml's person.name). Both give you compile-time-checked field names instead of a Ruby hash's free-form, unchecked keys.
Non-destructive update syntax matches closely
data Person = Person { name :: String, age :: Int } main :: IO () main = let alice = Person { name = "Alice", age = 30 } olderAlice = alice { age = 31 } in print (age olderAlice)
type person = { name : string; age : int } let () = let alice = { name = "Alice"; age = 30 } in let older_alice = { alice with age = 31 } in Printf.printf "%d\n" older_alice.age
The "copy with one field changed" idiom is nearly identical syntax in both: Haskell's alice { age = 31 } and OCaml's { alice with age = 31 } both build a brand-new record sharing every field except the one named, leaving the original completely untouched in both languages.
OCaml can declare a single field mutable; Haskell cannot
-- Haskell records are always immutable; "mutating" one field means -- building a whole new record via the update syntax shown above — -- there is no way to mark a single field as directly mutable in place. data Counter = Counter { count :: Int } main :: IO () main = let counter = Counter { count = 0 } incremented = counter { count = count counter + 1 } in print (count incremented)
type counter = { mutable count : int } let () = let counter = { count = 0 } in counter.count <- counter.count + 1; Printf.printf "%d\n" counter.count
OCaml lets a record declare any individual field mutable, and the <- assignment operator then updates that one field in place, on the existing record, with no new allocation and no wrapper type. Haskell has nothing resembling this at the record level — every Haskell record field is immutable, full stop, and the only way to "change" one is the copy-and-update syntax from the previous example, which always allocates a new record.
Modules vs. Type Classes
Ad-hoc polymorphism: type classes versus modules
class Describable a where describe :: a -> String data Dog = Dog instance Describable Dog where describe _ = "a dog" main :: IO () main = putStrLn (describe Dog)
module type Describable = sig type t val describe : t -> string end module Dog : Describable with type t = unit = struct type t = unit let describe () = "a dog" end let () = print_endline (Dog.describe ())
This is the deepest structural difference between the two languages, and it has no clean one-to-one mapping. Haskell resolves "which implementation for which type" through type classes, with the compiler picking the right instance automatically based on the type at the call site. OCaml has no type classes; the closest analogue is a module signature (module type) implemented by a specific module, but the caller must know and reference the module explicitly (Dog.describe) — there is no automatic, type-directed dispatch the way Haskell's typeclass resolution provides.
A module signature versus a type class interface
class Shape a where area :: a -> Double data Square = Square Double instance Shape Square where area (Square side) = side * side main :: IO () main = print (area (Square 4.0))
module type Shape = sig type t val area : t -> float end module Square : Shape with type t = float = struct type t = float let area side = side *. side end let () = Printf.printf "%g\n" (Square.area 4.0)
A Haskell type class declares a set of required functions that any conforming type must implement, and so does an OCaml module signature — both are, at heart, an interface. The key difference in practice is that Haskell allows many types to share one class and be used interchangeably wherever that class is required (via polymorphism over the class), while an OCaml module implementing a signature is used through its own explicit name — there is no equivalent to calling generic code against "any type implementing this signature" without extra machinery (functors, see below).
Functors: modules parameterized by other modules
-- Haskell has no direct equivalent to a functor in this sense — the closest -- comparison is a typeclass with a superclass constraint, which is a very -- different mechanism (constraining a type, not parameterizing a module). class Container f where wrap :: a -> f a main :: IO () main = putStrLn "see afterword — no direct OCaml-functor equivalent exists"
module type Item = sig type t val to_string : t -> string end module MakePrinter (Contents : Item) = struct let print_item item = print_endline (Contents.to_string item) end module IntItem = struct type t = int let to_string = string_of_int end module IntPrinter = MakePrinter (IntItem) let () = IntPrinter.print_item 42
An OCaml functor — confusingly, an entirely different concept from Haskell's Functor typeclass, despite the shared name — is a function from modules to modules: MakePrinter takes any module matching the Item signature and produces a brand-new module built around it. This is higher-order module programming with no real Haskell equivalent; Haskell's typeclasses solve a related problem (making code generic over a type) through a completely different mechanism (dictionary-passing resolved by the type at the call site, not an explicit module application written by the programmer).
Error Handling
Exceptions are the OCaml default; pure values are the Haskell default
import Control.Exception (try, SomeException) divide :: Int -> Int -> Int divide _ 0 = error "division by zero" divide x y = x `div` y main :: IO () main = do result <- try (print (divide 10 0)) :: IO (Either SomeException ()) case result of Left err -> putStrLn ("caught: " ++ show err) Right _ -> return ()
exception Division_by_zero let divide x y = if y = 0 then raise Division_by_zero else x / y let () = try Printf.printf "%d\n" (divide 10 0) with Division_by_zero -> print_endline "caught: division by zero"
Both languages have real exceptions, but the cultures differ sharply. In idiomatic OCaml, raising and catching exceptions (exception, raise, try ... with) is the everyday, first-reached-for way to signal failure — helped along by strict evaluation, where an exception fires predictably at the moment the failing expression is actually evaluated. Idiomatic Haskell reaches first for pure values — Maybe and Either — specifically because laziness makes it hard to predict exactly when a thrown exception would actually fire, so exceptions are reserved for truly exceptional, unrecoverable situations.
`Either` and OCaml's `result` are the same shape
safeDivide :: Int -> Int -> Either String Int safeDivide _ 0 = Left "division by zero" safeDivide x y = Right (x `div` y) main :: IO () main = print (safeDivide 10 0)
let safe_divide x y = if y = 0 then Error "division by zero" else Ok (x / y) let () = match safe_divide 10 0 with | Ok value -> Printf.printf "%d\n" value | Error message -> print_endline ("Error: " ^ message)
Haskell's Either String Int (Left for failure, Right for success) and OCaml's built-in result type (Error for failure, Ok for success) are structurally the exact same two-constructor sum type, just with more purpose-specific constructor names in OCaml's version — a small but genuinely helpful readability improvement over the more generic Left/Right.
`error` and `raise` both abort the program
import Control.Exception (evaluate, try, ErrorCall) main :: IO () main = do result <- try (evaluate (if False then 1 else error "unreachable state")) :: IO (Either ErrorCall Int) case result of Left err -> putStrLn ("caught: " ++ show err) Right value -> print value
let () = try let value = if false then 1 else failwith "unreachable state" in Printf.printf "%d\n" value with Failure message -> Printf.printf "caught: %s\n" message
Haskell's error and OCaml's failwith both raise an exception carrying a message and unwind the program if uncaught, and both are meant strictly for "this should be impossible" situations rather than ordinary, expected failure — the same escape hatch, reached for the same way, in both languages.
Laziness vs. Strictness
The one deep difference underlying almost everything else
main :: IO () main = -- The second argument to 'const' is never evaluated at all — Haskell is -- lazy by default, so an unused expression, however expensive, simply -- never runs. print (const 42 (error "this never actually runs"))
(* OCaml is strict by default: every argument is evaluated before the function body runs, whether or not the body actually uses it. This would raise the exception immediately, rather than never running it: *) let ignore_second _ second = ignore second; 42 let () = Printf.printf "%d\n" (ignore_second 42 99)
This is the single deepest difference between the two languages, and nearly everything else on this page follows from it. Haskell evaluates lazily by default: an expression is not computed until (and unless) its value is actually demanded, so const 42 (error "...") genuinely never evaluates its second argument. OCaml evaluates strictly (eagerly) by default: every function argument is fully evaluated before the function body runs, full stop — calling the equivalent function with a failing expression as an argument would raise immediately, whether or not the body ever looks at that argument.
Infinite lists: automatic in Haskell, explicit in OCaml
main :: IO () main = print (take 5 (map (* 2) [1 ..]))
let rec from number () = Seq.Cons (number, from (number + 1)) let naturals = from 1 let () = naturals |> Seq.map (fun number -> number * 2) |> Seq.take 5 |> Seq.iter (fun number -> Printf.printf "%d " number)
Because Haskell is lazy by default, an infinite list like [1 ..] is completely ordinary — take 5 only forces the first five cons cells, and the rest are simply never computed. OCaml's ordinary list is fully strict and cannot represent an infinite list at all (building one would loop forever trying to construct the whole thing). OCaml provides a separate type, Seq.t, built from explicitly lazy thunks (fun () -> ...), specifically for this use case — laziness is available, but it is an opt-in, explicitly-typed choice rather than the default evaluation strategy.
Predictable evaluation order versus unspecified order
import Debug.Trace (trace) main :: IO () main = -- The order "first"/"second" print in is not actually guaranteed by the -- language semantics here — only IO forces a specific sequence. print (trace "first" 1 + trace "second" 2)
(* OCaml guarantees left-to-right evaluation order for many constructs, including function arguments in most common cases, so side effects happen in a genuinely predictable sequence: *) let side_effect label value = print_endline label; value let () = Printf.printf "%d\n" (side_effect "first" 1 + side_effect "second" 2)
Because pure Haskell code has no inherent notion of "when," the language specification deliberately leaves evaluation order of pure expressions unspecified except where IO or explicit sequencing forces an order (Debug.Trace.trace is a debugging escape hatch precisely because ordinary pure code offers no such guarantee). OCaml's strict, eager model gives it a concrete, specified evaluation order for these same constructs, so interleaved side effects in ordinary "pure-looking" code are predictable in a way Haskell never promises.
Classic Examples
Quicksort — equally elegant, for the same reason
quicksort :: [Int] -> [Int] quicksort [] = [] quicksort (x : xs) = quicksort [a | a <- xs, a < x] ++ [x] ++ quicksort [a | a <- xs, a >= x] main :: IO () main = print (quicksort [3, 1, 4, 1, 5, 9, 2, 6])
let rec quicksort = function | [] -> [] | pivot :: rest -> let smaller = List.filter (fun value -> value < pivot) rest in let greater_or_equal = List.filter (fun value -> value >= pivot) rest in quicksort smaller @ [pivot] @ quicksort greater_or_equal let () = quicksort [3; 1; 4; 1; 5; 9; 2; 6] |> List.iter (fun number -> Printf.printf "%d " number)
This is close to a line-for-line transliteration — both versions pick the first element as pivot, filter the rest into two partitions with the same two predicates, and recursively sort and reassemble them with concatenation (++ in Haskell, @ in OCaml). The near-identical elegance here is not a coincidence: pattern matching on lists plus list comprehension/filter idioms are a shared strength of the whole ML/Haskell family.
Fibonacci, and where memoization actually lives
fibonacci :: Int -> Integer fibonacci 0 = 0 fibonacci 1 = 1 fibonacci n = fibonacci (n - 1) + fibonacci (n - 2) main :: IO () main = print (map fibonacci [0 .. 10])
let rec fibonacci n = if n = 0 then 0 else if n = 1 then 1 else fibonacci (n - 1) + fibonacci (n - 2) let () = List.init 11 fibonacci |> List.iter (fun number -> Printf.printf "%d " number)
Both naive versions recompute the same subproblems exponentially many times, with identical two-base-case, one-recursive-case structure. Adding memoization diverges sharply, though: idiomatic OCaml reaches for a genuinely mutable Hashtbl cache updated in place as a side effect (natural in a strict, mutation-friendly language), while idiomatic Haskell instead builds a lazily-evaluated, self-referential infinite list or array of results — memoization achieved entirely through laziness, with no mutable cache at all.
Tree depth, recursing over a shared shape
data Tree = Leaf | Node Tree Tree depth :: Tree -> Int depth Leaf = 0 depth (Node l r) = 1 + max (depth l) (depth r) main :: IO () main = print (depth (Node (Node Leaf Leaf) Leaf))
type tree = | Leaf | Node of tree * tree let rec depth = function | Leaf -> 0 | Node (left, right) -> 1 + max (depth left) (depth right) let () = Printf.printf "%d\n" (depth (Node (Node (Leaf, Leaf), Leaf)))
One last look at how closely the two languages align on the exact combination this page keeps returning to — a recursive algebraic data type plus a structurally recursive function pattern-matching over it. Swap data/type, | placement, and capitalization conventions, and the two definitions are otherwise a direct translation of each other, right down to using the same built-in max function by the same name.