let bindings that never change
A name in F# is a definition rather than a slot: there is nothing to assign to, so a value you read at line three still means the same thing at line thirty.
A binding is a definition
let price = 12 does not put 12 into a box called price. It defines price to mean 12 for everything below it, and there is no syntax for changing it afterwards: no re-assignment, no ++, no +=. When you want a different value you write a new line with a new name.
This sounds like a restriction and behaves like a guarantee. A name you read on line three says the same thing on line thirty, so a piece of arithmetic can be checked where it is written, without hunting for the line that changed one of its inputs.
A new name, not a new value
When the price goes up, the program gets a second binding worked out from the first: let cheaper = price - 5. Both names exist; neither moves. total was computed from the price as it was, and it keeps that answer.
Inside a function a name may be defined again and the nearest definition wins below it. That is shadowing, and it is how a value is refined in steps without inventing text1 and text2. At the top level of a script, though, a name may be defined only once: a second let price is rejected with FS0037: Duplicate definition of value, which is the rule that keeps a file a list of definitions rather than a sequence of edits.
Defining values, then refining one
let price = 12
let count = 3
let total = price * count
let label n =
let text = "books"
let text = sprintf "%d %s" n text
textFour bindings and a shadowed name
Run it and check that `total` kept its answer after `cheaper` was defined. Then add a binding for a discount of 2 and print it beside the price.
let mutable exists, and so does <-. This course uses neither. Once a program can change a value in place, every question about that value becomes a question about when, and the chapters ahead get by without asking it.
A script is read from the top
Every definition joins the scope of the file below the line that declares it. A function may use values and functions defined above it, never below, which is why a helper has to appear before its caller and a type before the record that uses it. A script has no hoisting and no forward declaration to work around that.
Inside a function the rule repeats at a smaller scale. Parameters and local let bindings live in the body, below the point where they are written, and disappear when the function returns. A local name may shadow an outer one; the new definition wins only in the lines that follow it, and the outer value is still there for everything after the function.
The order is therefore a diagram of what depends on what. If a function will not compile because a name is not defined, the fix is usually to move a definition up rather than to reach for a different kind of binding.
What the guarantee buys
Because no later line can change an earlier value, you can replace a name with the expression it was bound to and keep reading. That is what makes a small function safe to extract and a calculation safe to move: the answer travels with the definition instead of depending on when it is read.
The price is that a running total has to be passed along rather than updated in place. The fold in a later chapter is exactly that idea: the state goes in as an argument and comes out as an answer, and nothing is left behind to change. A function that returns a new total is also easier to test, because its answer depends only on its arguments.
At the top level of a script, the definitions are compiled in order into a sequence of statements, so a later let can use an earlier one but not the other way round. In a compiled F# file the same rule holds inside a module, and the module boundary is what lets one file refer to another: the compiler reads the files in the order the project lists them.
Because a binding cannot change, a stack trace or a debugger watch is easier to trust: the value shown for a name is the value the expression had when it was evaluated, not a later state that happens to share the name.
At the top level a second let price is FS0037, not an update; shadowing only works in an inner scope such as a function body. price = 12 is a comparison and answers a bool, so it cannot assign. And a forward reference fails with "not defined" rather than finding the line below.
Try these next
- Define
widthandheight, thenarea; print all three, then moveareaabovewidthand read the error. - Write a function that shadows its parameter with a trimmed version, and print the original argument afterwards.
- Try
let total = total + 1afterlet total = 10and explain why the second line cannot work.
A receipt refined in steps
Run it and read the two price lines, then watch the name arrive in capitals. Then add a second local refinement that appends the word `paid` a second time, and show that the argument outside the function is unchanged.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
After let price = 12 and let cheaper = price - 5, what is price?
