- Courses
- 💻Programming Languages
- 🐫OCaml (WIP)
- Type Inference & Higher-Order Functions
- Reading Unfamiliar Type Signatures
On this page
Developing Fluency in Reading Unfamiliar Type Signatures
Published on July 21, 2026
By Enrico Hochkoeppler
Web Developer & TutoriaLibre Founder
A type signature in OCaml is a complete, precise specification of what a function can possibly do, and with enough practice, it can be read as fluently as a sentence. This is a skill worth building deliberately, since real OCaml codebases and libraries are full of functions whose signature is the only documentation immediately available — being able to reconstruct a function's likely behavior from ('a -> bool) -> 'a list -> 'a list alone, before ever opening its implementation, is one of the most practically useful abilities to develop.
Step One: Split on the Arrows, Right to Left
Every arrow in a type signature separates one argument from the rest, and -> is right-associative, so int -> int -> int really means int -> (int -> int). The practical consequence: whatever comes after the last arrow is the final return type, and everything before it, one arrow at a time, is an argument.
int -> string -> bool -> unit
reads as: takes an int, then a string, then a bool, and returns unit. Making this concrete by rewriting the signature using nested fun expressions removes any doubt about the grouping:
fun x -> (fun y -> (fun z -> (* body using x, y, z *)))
This is exactly what a three-argument curried function's type is describing — three arguments taken one at a time, with the innermost function's body finally producing the result.
Step Two: Repeated vs. Distinct Type Variables
Once a signature contains type variables, they carry real information about what relationships the function is allowed to rely on. A repeated variable forces two positions to be the exact same type; distinct variables leave them free to differ.
'a -> 'a -> bool
'a -> 'b -> bool
The first signature can only be applied to two values of the same type — comparing an int to a string wouldn't type-check. The second can be applied to two values of different types, since 'a and 'b are independent. Neither signature reveals what the function actually checks, but both already narrow the space of plausible implementations considerably before a single line of the function's body has been read.
Step Three: What a Fully Generic Type Variable Rules Out
A function that receives a value of a purely generic type 'a, with nothing else constraining it, cannot do anything to that value that depends on its concrete type — it can't compare it, print it, do arithmetic on it, or inspect its structure. It can only store it, return it, pass it along, or discard it entirely. This single fact is a surprisingly powerful tool for guessing what a function does. Consider:
'a -> 'a -> 'a
Given only this signature, the possibilities are already sharply limited: the function must return one of its two arguments — there's no third 'a value it could conjure out of nowhere — so it's most likely something like "return the first one," "return the second one," or "return whichever one satisfies some condition decided some other way." A signature like this belongs to functions such as choosing a default, a maximum, or a first-come-first-served value — never to something that inspects or transforms the values themselves.
Step Four: Reading a Function Argument as a Signature of Its Own
A higher-order function's signature contains a smaller signature nested inside parentheses, and that inner signature deserves to be read the exact same way before moving on to the rest:
('a -> bool) -> 'a list -> 'a list
Reading left to right: the first argument, ('a -> bool), is itself a function from any type to bool — recognizably a predicate. The second argument is a list of that same type. The return type is a list of that same type again. Putting this together: a predicate is used to somehow narrow down a list of the same element type, returning a list of the same element type — a strong hint that this is a filtering operation, even without having seen a single line of its implementation or its name.
Step Five: What Appears Only Once Is Often Discarded
Pay attention to which type variables show up in the return type and which don't. A signature like
('a -> 'b) -> 'a list -> 'b list
has 'b appear only after the function is applied, meaning the original 'a elements themselves are gone from the final result — they've been transformed into something else and not carried forward directly. Compare this to
('a -> bool) -> 'a list -> 'a list
where 'a survives all the way through to the return type unchanged, meaning whatever this function does, it isn't allowed to alter the values themselves — only to include or exclude them.
Step Six: Labeled and Optional Arguments Read a Little Differently
A signature containing a label, written ~name:type, or an optional argument, written ?name:type, still separates by arrows the same way, but the order labels appear in doesn't have to match the order they're supplied at the call site the way plain positional arguments do:
delta:float -> x:float -> y:float -> f:(x:float -> y:float -> float) -> float
This reads as: a function taking a labeled delta, a labeled x, a labeled y, and a labeled function f (which itself takes labeled x and y and returns a float), producing a final float. The presence of labels is itself informative: it signals that several of the arguments share the same underlying type (here, several plain float values), and that without labels attached, a caller would otherwise have no obvious way to keep, say, x and y straight at the call site — precisely the situation labels exist to prevent.
A Full Worked Example
Consider encountering this signature for the first time, with no other documentation:
('a -> bool) -> 'a list -> 'a option
Working through it step by step: the first argument is a predicate over some type 'a; the second is a list of that same type; the return type is an option of that same type, not a list. Since option represents "maybe present, maybe not," and the input is a list being examined with a predicate, this signature is very strongly suggestive of "search a list for an element satisfying a predicate, returning it wrapped in Some if found, or None if the list contains no such element" — which is, in fact, exactly the signature of List.find_opt. Reaching that conclusion never required looking at a single line of the function's actual definition.
Key Takeaways
- Arrows in a type signature associate to the right; everything after the final arrow is the return type, and everything before it, one arrow at a time, is an argument.
- A repeated type variable forces two positions to match; distinct type variables leave them free to differ — both are meaningful constraints on what a function could possibly be doing.
- A function receiving a fully generic value can't do anything type-specific with it, which sharply narrows the plausible behaviors behind even a short, unfamiliar signature.
- A function-typed argument in parentheses is a signature in its own right and deserves to be read the same way before moving on to the rest of the outer signature.
- Whether a type variable survives into the return type or disappears after it reveals whether a function transforms its input or only filters, inspects, or reorders it.
- Labeled and optional arguments relax positional ordering at the call site but still separate by arrows in the signature itself; their presence often signals several arguments sharing an underlying type that would otherwise be easy to mix up.
Found this helpful? Share the knowledge...
... or support this project to keep it alive!
Previous
Imperative Loops: For, While
Next
Exercise: Build Your Own List Module
- Step One: Split on the Arrows, Right to Left
- Step Two: Repeated vs. Distinct Type Variables
- Step Three: What a Fully Generic Type Variable Rules Out
- Step Four: Reading a Function Argument as a Signature of Its Own
- Step Five: What Appears Only Once Is Often Discarded
- Step Six: Labeled and Optional Arguments Read a Little Differently
- A Full Worked Example
- Key Takeaways
