Operators, precedence, and common pitfalls
You already know most of OCaml's operators from school arithmetic
and from previous lectures (the
tour introduced +, *, /,
mod; the literals lecture
contrasted + and +.). This lecture is the comprehensive
reference. It lays out the full set, says which bind tighter than
which, and walks through the small set of mistakes that beginners
reliably make in their first week. There is nothing deep here, but
a lot of it is sharp-edged: every one of the pitfalls in the second
half of the lecture has caught me out at some point.
The reason to have a dedicated lecture on operators is that OCaml
makes some unusual choices: separate arithmetic operators for int
and float, structural-not-physical equality as the default, and a
restricted notion of polymorphic comparison. These choices have good
reasons (we have argued for them throughout Module 1 and the first
half of Module 2), but they generate a predictable set of beginner
type errors. Pre-reading those errors here will save you debugging
time later.
Arithmetic, by type
OCaml has separate arithmetic operators for int and float. The
float versions all carry a trailing dot. You have seen this before;
the full table is worth having in one place.
A few details worth flagging:
Division. a / b on int is truncating: it throws away the
fractional part. So 7 / 2 = 3, not 3.5. The companion is a mod b, the integer remainder. For floats, a /. b is the ordinary
mathematical division, returning float.
Power. OCaml's ** operator is float exponentiation:
2.0 ** 10.0 = 1024.0. The standard library has no built-in
integer power. For small powers, spell out the multiplication
(let cube x = x * x * x); for arbitrary integer powers, write a
small recursive helper (let rec pow a b = if b = 0 then 1 else a * pow a (b - 1)).
Negation. Unary negation on int uses the same - symbol as
subtraction, but it sits in front of a single argument: let x = -5. For floats, the unary negation is -. (with a trailing dot):
let y = -. 3.14. This is the only case where you write -. as a
prefix operator instead of an infix one.
Absolute value. abs for int, Float.abs for float. The
stdlib used to have abs_float; that name is deprecated in
favour of Float.abs. Either works in current OCaml.
The two-operators-per-arithmetic rule is the most distinctive
thing about OCaml arithmetic and the source of most beginner type
errors. Internalise: + for ints, +. for floats; * for ints,
*. for floats; etc. Module 2 will burn this into your fingers.
mod is the integer remainder. There is no mod. operator for
floats; if you need float remainder, use Float.rem a b from the
standard library.
Comparison and equality
The comparison operators (<, <=, >, >=) and the logical
operators (&&, ||, not) were introduced with
booleans. A quick recap of the
logical side: && and || short-circuit, exactly as in C / Java
/ Python, and negation is the standard-library function not,
not the symbol !.
Equality deserves the fuller treatment we promised back in the tour of OCaml. OCaml has two equality operators, and they answer different questions:
=is structural equality: do the two values have the same contents? It compares recursively (two ints are=when they are the same number, two strings when they have the same bytes, two pairs when their components are correspondingly=) and it is polymorphic: the one operator works on ints, floats, strings, pairs, and most other data. Its negation is<>.==is physical equality: are the two values the same object in memory? Its negation is!=.
Structural equality is the question everyday code asks (is the
input the string "quit"?), so the rule for beginners is simple:
always write =. Physical equality only matters in advanced code
that cares about sharing and mutation; when you meet == in the
wild, read it as a deliberate, expert-level choice.
The two operators can disagree. A pair like (1, 2) bundles two
values into one (pairs get a proper introduction later in the
course); here are two pairs built separately, with the same
contents:
p and q are structurally equal but physically distinct: each
(1, 2) allocated its own pair. This is the trap for programmers
arriving from C or Java, where == is the everyday equality
operator: an OCaml == test compiles fine and then returns
false for values you can plainly see are equal. If an equality
test in your code is mysteriously failing, check the operator
first.
One caveat to file away: structural equality works on data, but
it raises a runtime exception (Invalid_argument "compare: functional value") if the values being compared contain
functions, because there is no general way to decide whether two
functions behave identically.
String concatenation
Strings concatenate with ^, not +:
^ is right-associative: "a" ^ "b" ^ "c" parses as "a" ^ ("b" ^ "c"). This is mostly invisible (the result is the same either
way), but it matters for performance on long chains: right
association means the leftmost strings are concatenated last, so
each intermediate result keeps growing. For a few strings, fine.
For many, use String.concat:
String.concat sep xs joins the elements of xs with sep
between them. It allocates the result string once, of exactly the
right size; it is dramatically faster than ^-chaining when you
have dozens or hundreds of pieces.
For formatted output, Printf.sprintf is the standard tool:
The format string "%d" is the C-style integer specifier. We
will see Printf in more depth later.
Function application is its own "operator"
Function application in OCaml is juxtaposition: just write the function next to its arguments, separated by spaces. No parentheses or commas.
Function application binds tighter than any infix operator, so
succ 5 + 3 parses as (succ 5) + 3 = 9, not succ (5 + 3) = 9.
(They give the same answer here by coincidence; in general the
two parses would differ.)
Function application is left-associative: f x y means (f x) y. So when you nest calls, you need parentheses to group:
Without the parentheses, OCaml would parse this as succ max 3 7,
i.e. ((succ max) 3) 7: try to apply succ to max, which the
compiler rejects.
The "no parentheses on function call" rule takes adjusting to if you came from C-family languages. The reason OCaml does this is that it makes partial application (supplying some but not all arguments and getting back a function) a natural reading. We will see partial application in Module 3.
Operator precedence
Here is OCaml's operator precedence, tightest at the top, loosest at the bottom. Levels separated by horizontal lines bind tighter than levels below.
A few observations:
- Function application is one of the tightest forms. Tighter than any infix operator. This is unusual; in many languages, function call has the same precedence as parenthesisation.
- Arithmetic follows the school order:
*,/,modtighter than+,-. Same as everywhere. - Comparisons sit below arithmetic, so
1 + 2 < 5parses as(1 + 2) < 5, as expected. &&binds tighter than||, same as everywhere.- The tuple constructor
,binds very loosely, so1, 2 + 3is(1, 5), not(1, 2) + 3.
When in doubt, just parenthesise. Explicit parentheses cost nothing at runtime and make the parse intent crystal clear to the reader. Code is read far more often than it is written; spend the ten extra keystrokes.
mod is at the same precedence level as * and / (and is
left-associative). So 10 mod 3 * 2 is (10 mod 3) * 2 = 2, not
10 mod (3 * 2).
Pitfall 1: + instead of +.
By far the most common type error in your first week:
The compiler refuses with:
Error: The constant 3.14159 has type float
but an expression was expected of type int
The fix is 3.14159 *. r *. r (note the three dots). The error
message is helpful once you can read it: it says "expected int"
because * is the integer-multiplication operator; it names the
constant 3.14159 and says it has type float because that is a
float literal. The mismatch tells you which operator is wrong.
Pitfall 2: implicit conversion that isn't there
In Python and JavaScript, you can write "value: " + 5 and the
language coerces the int to a string. OCaml does not:
^ is string concatenation; both operands must be string. To
mix an int in, convert explicitly with string_of_int. For
richer formatting (decimal precision, padding, hex, scientific
notation), Printf.sprintf is the go-to.
The lack of implicit conversion is a feature, not a bug. Languages
that do coerce automatically have famously confusing edge cases
(JavaScript's 1 + "1" == "11" but 1 - "1" == 0; Python's
"strict but with surprises"). OCaml's "always be explicit" rule
means you read code and know exactly what conversion is happening.
Pitfall 3: subtraction looks like unary minus
Looks like "absolute value of negative 5." Actually parses as "abs minus 5":
abs -5 is parsed as abs - 5: take the function abs, subtract
5 from it. That doesn't type-check (you cannot subtract from a
function), so you get an error. The fix is to parenthesise the
negative literal: abs (-5). Same with floats: Float.abs (-. 3.14).
This catches everyone at least once. When you have a unary minus in argument position, parenthesise it.
Pitfall 4: comparison chains are not a thing
In Python, 0 < x < 10 reads as you'd hope: "x is between 0 and
10." Python is unusual in supporting this; OCaml (like most
languages) does not (we bind x to 5 so the chain itself is
the only error):
OCaml parses this as (0 < x) < 10: first compare 0 < x, which
gives bool, then compare that bool to 10. The polymorphic
< wants both operands at the same type, so the compiler refuses
with "The constant 10 has type int but an expression was expected
of type bool". The fix is to write the bounded check with &&:
This idiom (a < x && x < b) is so common that you internalise it
quickly.
A quick check
What is the value of this OCaml expression?
falsetrue- A type error:
intcompared tobool. 7
Why: apply precedence. * binds tighter than +, so 2 * 3 = 6. Then +: 1 + 6 = 7. Then =: 7 = 7 is true. Then &&:
true && true is true. Reading the implicit parentheses: `(((1
- (2 * 3)) = 7) && true)
. The expression has typebooland valuetrue`.
A code challenge to close out:
Write in_range : int -> int -> int -> bool that returns true
exactly when x lies in the closed interval [lo, hi]. Use the
&&-idiom this lecture introduced. Argument order:
in_range lo hi x.
Show reference solution
let in_range lo hi x = lo <= x && x <= hi. The && short-circuits,
so the upper-bound check only runs when the lower-bound check
already passed.
What's next
Next lecture: if/then/else as
an expression. The big conceptual shift is that if returns a
value in OCaml: it is not a statement that controls execution
flow, but an expression that evaluates to one of two values. The
downstream consequence is that you can use if anywhere an
expression can go: as a function argument, as the right-hand side
of a let, inside another if.
Reading
- OCaml manual, Expressions (operator section): the authoritative precedence table: https://v2.ocaml.org/manual/expr.html
- Cornell CS3110, Operators: a friendlier walk-through: https://cs3110.github.io/textbook/chapters/basics/expressions.html
Sources
This lecture's prose, worked examples, and quizzes are original to
this course. Materials referenced during preparation are listed in
the Reading section above; Cornell CS3110 and Real World OCaml
are CC BY-NC-ND-licensed and have not been derivatively reused.
See LICENSES.md
at the repository root for the full source posture.