Names, values and let
Bindings with let, the difference between a name and a box, let ... in for a name with a short life, and the types the compiler works out for you.
A name, not a box
let gives a value a name. The name is not a box that can be filled later: it is a shorthand for the value, and a second let on the same name makes a new binding that hides the old one for the rest of the file. Nothing is mutated, so a name always means what it meant where it was written.
That is why reading OCaml is easy on the memory: no function can change a value behind your back between two lines. When a program seems to change something, it has built a new value and given it a name — often the same name, which is why a second let is worth noticing. A reader who keeps that in mind never has to ask what a name holds at a given line: it holds what the nearest let above says.
A name with a short life
Inside an expression, let ... in binds a name for the rest of that expression only. It is how a function holds on to an intermediate answer without adding a definition to the whole program, and it is the shape you will use again and again for a value that is needed twice.
OCaml works out types instead of being told them: let n = 3 makes an int, let pi = 3.14 a float, and a caret between two strings makes a string. The same name may be used at another type later, because the earlier binding is hidden rather than changed.
How a name is looked up
When the compiler meets a name, it looks for the nearest let above it. A let ... in adds one frame for the expression that follows; a top-level let adds a name for the rest of the file. Leaving that frame, at the end of the expression, makes the inner name disappear again, which is why the same word can mean two things in two places.
Evaluation follows the text. let doubled = count * 2 in ... computes doubled once, before the body runs, and the body sees that value. Nothing is recomputed when the name is used again, which matters when the expression prints something or costs real work.
Type inference runs in the same pass. A name starts with no type at all; the first operation that uses it fixes the type, and every later use has to agree. That is why let n = 3 is an int before you print it, and why adding 0.5 to it is refused rather than quietly converted.
A binding is a definition, not a statement
In a language with statements, a line like let n = 3 would do something and move on. In OCaml it is a definition: it adds n to the environment, and the environment is what the rest of the file is checked against. There is no run-time moment when the name is created and later assigned; the compiler has already resolved every use to the value it denotes.
This is also why the toplevel can report the type of a binding as soon as you write it. The definition is complete at the moment it ends: the value is known, the type is known, and the name is available. Nothing about it waits for a later line.
What goes wrong
The first confusion is assignment. n = n + 1 is a comparison, so it answers false and changes nothing; a script that expects the number to grow keeps printing the old one. If a line looks like a statement and the next line disagrees, check for a single = where you meant let.
The second is scope. A let without in, inside an expression, ends the expression: the compiler reports a syntax error near the next token, not at the let. When an error points at a line that looks fine, look at the line above for a binding that was never followed by in.
Three habits from other languages cause trouble here. n = n + 1 on its own is a comparison, not an assignment, and its answer is a bool. A let inside a sequence needs in, or the compiler reads a definition where an expression was expected. And after let n = 3, a later let n = 4 does not change the old n for code written above it; it hides the name below. If a value seems to change, look for the second let.
Try these next
- Bind a temperature in Celsius, then a local
fahrenheitwithlet ... in, and print both. - Shadow a name with a different type, such as an int then a string, and print each before the shadow.
- Write the same arithmetic with and without a local name, then check that the answers match.
One name, two scopes
let city = "Kyoto"
let temp = 21
let () =
let warmer = temp + 4 in
print_endline (city ^ " is " ^ string_of_int temp);
print_endline ("warmer by " ^ string_of_int (warmer - temp))Shadowing, and a local name
Run it and read the six lines. Then make the inner `greeting` print in capitals as well, and add a `let ... in` that holds the value of `count + 10` for one line only.
A total with a local name
Run it, then change the price and the count. Add a local `discount` inside the first block and print the total after subtracting it.
Two bindings with the same name are two different names: the later one hides the earlier one for the rest of the file. That is not assignment, because nothing that already used the old value can see the new one, and it is what makes a let safe to read on its own.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
After let n = 3 and then let n = n + 1, what does n name?
