Operators, precedence, and common pitfalls

Functional Programming with OCaml

Operators, precedence, and common pitfalls

Module 2 · Lecture 4

KC Sivaramakrishnan
IIT Madras

This lecture: operators

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.

Arithmetic, by type

Operation int float
Add a + b a +. b
Subtract a - b a -. b
Multiply a * b a *. b
Divide a / b (truncating) a /. b
Remainder a mod b (Float.rem a b)

Arithmetic, by type: power, negate, abs

Operation int float
Power (no built-in; write x * x * x) a ** b
Negate -a -. a
Absolute abs a Float.abs a

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:

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:

let p = (1, 2) let q = (1, 2) let _ = p = q (* = true : same contents *) let _ = p == q (* = false : two distinct objects in memory *) let _ = p == p (* = true : literally the same object *)

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 +:

let _ = "first" ^ " " ^ "second" (* = "first second" *)

String concatenation

let _ = "first" ^ " " ^ "second" let _ = String.concat ", " ["apple"; "banana"; "cherry"]

^ 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:

let _ = String.concat ", " ["apple"; "banana"; "cherry"] (* = "apple, banana, cherry" *)

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:

let _ = Printf.sprintf "value: %d" 5 (* = "value: 5" *)

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.

let _ = succ 5 (* = 6 *) let _ = max 3 7 (* = 7 *) let _ = String.length "hello" (* = 5 *)

Function application is its own "operator"

let _ = succ 5 let _ = max 3 7 let _ = String.length "hello" let _ = succ (max 3 7)

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:

let _ = succ (max 3 7) (* = 8 *)

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.

Operator precedence (tightest to loosest)

Lvl Operators Notes
1 . record / module access
2 \(f\ x\) function application
3 *, /, mod, *., /. multiplicative
4 +, -, +., -. additive
5 ^, @ string / list concat
6 <, =, >, <=, >=, <> comparisons
7 && logical and
8 || logical or
9 , tuple constructor
10 ; sequence

A few observations:

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:

let area r = 3.14159 * r * r

The compiler refuses with:

Error: The constant 3.14159 has type float
       but an expression was expected of type int

Pitfall 1: + instead of +.

let area r = 3.14159 * r * r

OCaml refuses:

Error: The constant 3.14159 has type float
       but an expression was expected of type int

Fix: 3.14159 *. r *. r. The operator drives the type.

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:

let _ = "value: " ^ 5

Pitfall 2: implicit conversion that isn't there

let _ = "value: " ^ 5
Error: The constant 5 has type int but an expression was expected
       of type string
let _ = "value: " ^ string_of_int 5

Or Printf.sprintf for richer formatting:

let _ = Printf.sprintf "value: %d" 5

^ 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

let _ = abs -5

Looks like "absolute value of negative 5." Actually parses as "abs minus 5":

Pitfall 3: subtraction syntax

let _ = abs -5 let _ = abs (-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):

let _ = let x = 5 in 0 < x < 10

Pitfall 4: comparison chains aren't a thing

let _ = let x = 5 in 0 < x < 10 let _ = let x = 5 in 0 < x && x < 10 (* = true *)

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 &&:

let _ = let x = 5 in 0 < x && x < 10 (* = true *)

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?

let _ = 1 + 2 * 3 = 7 && true

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

Activity

How does OCaml parse:

let _ = 1 + 2 * 3 = 7 && true

What does it evaluate to? Trace through.

Activity discussion

let _ = 1 + 2 * 3 = 7 && true

Parse with precedence:

Answer: true. Implicit: ((1 + (2 * 3)) = 7) && true.

Any grouping that surprises you: candidate for explicit parens.

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.

let in_range lo hi x = failwith "not implemented"
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.

What's next

Reading

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.