Local names with let
let for names that live inside one piece of code, a map or a vector taken apart on the way in, and why naming a value once beats writing it twice.
A name that lives in one place
(let [a 1 b 2] (+ a b)) makes names for the body that follows and forgets them at the end. Nothing outside the let can see them, which is the whole point: a name that only one piece of code can reach cannot be broken by another. The body is the last form of the let, not the binding vector: everything after the closing square bracket runs with the names in place, and the let answers whatever that body answers.
The bindings are written as pairs inside one vector, and each pair can use the names made before it. A let is not a place to store things — it is a way to say a value once and give it a name, so the next line can be about something else. It is one expression whose middle has names, not a loop and not a run of statements, which is why a let can appear wherever a value is expected, including inside another let.
Taking a value apart on the way in
(let [[a b] [10 20]] ...) pulls a two-item value apart. (let [{:keys [who age]} person] ...) pulls values out of a map by the keys you name in the vector, and {:keys [who]} means the value at :who, called who. The vector on the left describes the shape of the value on the right: two slots for a vector, named keys for a map. That shape is a description rather than a check: a missing key or a short vector simply binds nil, which is why the default in the next section matters.
The same shapes work in the argument list of a defn, so pick whichever reads better at the call site: one value that happens to have parts, or a record whose fields you want by name.
Why bother naming anything
A name is a place to be wrong in one way only. Write the same expression three times and a mistake is in three places; name it once and the fix is in one. It also gives the reader a word for the value, which is usually worth more than the line it saves. A name gives you a place to put a comment too: subtotal says more than (* price qty) does, and the next line can be read without holding the multiplication in your head.
Locals are cheap and you can make as many as you like. When a line is hard to read, the fix is often to name the middle of it and let the last line be short. The cost of a local is one line and a pair of brackets; the cost of an unnamed expression is that every reader has to work it out again.
A default when the key is missing
Map destructuring can supply a default with :or: (let [{:keys [city] :or {city "unknown"}} person] ...) uses "unknown" when the map has no city key. A key that is present with nil is still nil, because the key is there. The :or map is written after the keys it applies to, and it only fills in names the pattern asked for; a default for a key the pattern never mentions does nothing.
The default is written next to the shape it belongs to, so the reader sees the fallback before the body uses the name. It does not change the map; it only decides what the local holds when the key is absent.
Use a default when absence is ordinary. When a missing key would be a mistake, leave the default out and let the nil show up where it matters.
Names inside a function, and a value taken apart
(defn bill [price qty]
(let [subtotal (* price qty)
tax (quot subtotal 10)
total (+ subtotal tax)]
(str "subtotal " subtotal
", tax " tax
", total " total)))
(def ada {:who "Ada" :age 36 :city "London"})
(let [{:keys [who age city]} ada]
(println (str who " is " age " in " city)))
(let [[a b] [10 20]]
(println (str a " then " b)))
(println (bill 40 3))A bill, and a record taken apart
Run it and read the three lines. Then add a let that names the number of items in a list and prints it, and change the bill so the tax is a fifth of the subtotal.
A local name hides an outer name of the same spelling for as long as the let runs, and the outer value comes back when it ends. That is not a problem to work around: it is how a small piece of code avoids having to think about the rest of the program.
Three things to watch. A binding cannot use the name it is defining: [x (inc x)] looks at an outer x, or fails if there is none. A let does not change a name defined outside it; the outer name is back as soon as the body ends, which is why printing it afterwards shows the old value. And def inside a let is not a local name — it creates a top-level name that outlives the let, which is almost never what you meant.
Try these next
- Write a let that names a price, a quantity and the product, then prints only the product.
- Take a three-item vector apart with
[a b c]and print the middle one. - Shadow an outer name inside a let, print it inside, then print it again outside.
A cinema bill with local names
Run it and read the five lines. Then add a `:day` key to the map and a let that prints a cheaper price on a weekday, and change the booking fee to 3.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Where can a name made by let be read?
