5 features of Clojure let
Clojure let
is used to define new variables in a local scope. These
local variables give names to values. In Clojure, they cannot be
re-assigned, so we call them immutable.
Here are a few things you probably know about let
, and a few you
don't.
Variable shadowing
In a Clojure let
, You can name your variables however you want---even
reusing the name of an existing variable. Of course, when you do that,
your code can only refer to the most local definition. There is no way
to access the outer one. Doing this is called shadowing. It's like the
outer variable is in the shadow of the local variable.
Example:
(def db "postgres://localhost:3456")
(defn fetch-users []
(let [db "postgres://my-server.com:2232"] ;; this variable shadows the global
(jdbc/query db "SELECT * FROM users"))) ;; refers to local db
Bonus:
You can even shadow variables within the same let
:
(let [s "Eric Normand"
s (str/upper-case s)
s (str/trim s)
s (str/replace s #" +" "-")]
(println s))
We are defining and redefining s
, which shadows the one just above it.
Notice, we are also referring to variables above, which is the next
topic.
Refer to variables defined above
Clojure let
allows you to define multiple variables. A common question
is whether they can refer to each other. So can they?
The variables you define in a let
can refer to each other, but only in
the order they are defined in. This one should work:
(let [x 1
y (* 2 x) ;; refer to x above
z (+ 4 y)] ;; refer to y above
(println z))
That's cool. But this one doesn't work:
;; this doesn't work
(let [person {:first-name first-name ; defined below
:last-name last-name} ; defined below
first-name "Eric"
last-name "Normand]
(prn person))
Why not? What's the difference? Well, you can only refer to variables
after they've been defined. let
s define variables in order, so you
can only refer "up", not "down".
Even with that limitation, Clojure let
's ability to refer to variables
you just defined is very useful, especially for multi-step actions.
Destructuring
The vector (square brackets []
) just after the let
in a let
expression is called the bindings. In Clojure, anywhere you have
bindings, you get to use destructuring. Destructuring is a convenient
way of defining multiple variables with values from a data structure.
It's much shorter than doing it all by hand.
Destructuring is a bigger topic that I can't go into in detail here. There's enough to learn about it that I made a short video course called Destructuring. Do check it out because it can seriously make your code cleaner and shorter.
Here is an example of how much shorter destructuring can really be:
(let [match (re-matches #"ab(c+)" "abccc")
s (get match 0)
g1 (get match 1)]
(println "string:" s "cs:" g1))
Now, with destructuring:
(let [[s g1] (re-matches #"ab(c+)" "abccc")]
(println "string:" s "cs:" g1))
We're able to pull out two values from within the vector on a single
line of let
binding, as opposed to three lines without destructuring.
Multiple expressions in the body
Clojure let
expressions can have multiple expressions after the
binding. Those expressions are called the body of the let
. The body
expressions are executed in order. The value of the let
is the value
of the last expression in the body.
Using the value of the last expression is useful because it lets you do related actions just before. Take this example of logging before a request goes out.
(let [url "https://purelyfunctional.tv"
data {:session "12345"}]
(log/info "Posting data to api" data)
(http/post url data))
All of the expressions in the body can refer to variables defined in the
binding form. The let
is like a neat package of variables and
expressions.
Local variables disappear after the closing parenthesis
Once you close the paren after the last expression in the body, those
variables you defined in your let
do not exist anymore. You cannot
refer to them. In fact, anything you shadowed is now unshadowed. You can
get at the pre-shadowed values.
Here's an example
(def my-name "Eric Normand")
(println my-name) ; refers to global `my-name`
(defn print-person-name [person]
(let [my-name (:name person)] ; shadows the global
(println my-name)) ; prints whatever name is in the person
(println my-name)) ; prints the global!
Again, this is really nice because it makes the let
a nice little
package. Shadowing is not the same as re-assigning.
Bonus: let
println
debugging trick
Sometimes you need to do a little println
debugging. Let's say your
code is failing somewhere inside of a let
binding. Here's your code:
(let [user (get-user user-id)
account (get-account user) ; this line fails
transaction (make-deposit account 100)]
(save-transaction transaction))
When you run it, you get an error on the line marked above. get-user
works, but get-account
is failing. But what is being passed to
get-account
? Maybe it's wrong.
You try to add a println
to inspect the value, but that doesn't work:
(let [user (get-user user-id)
(println user) ; doesn't compile
account (get-account user) ; this line fails
transaction (make-deposit account 100)]
(save-transaction transaction))
The compiler complains that you need an even number of forms in the binding, which makes sense. Every binding needs a variable and a value.
So how do you fix it? There's a trick. Just give it a throwaway variable
name. The most common one to use is _
(underscore).
(let [user (get-user user-id)
_ (println user) ; does compile
account (get-account user) ; this line fails
transaction (make-deposit account 100)]
(save-transaction transaction))
In fact, because you can reuse variable names, you can do the same trick all over.
(let [user (get-user user-id)
_ (println user) ; does compile
account (get-account user) ; this line fails
_ (println account)
transaction (make-deposit account 100)
_ (println transaction)]
(save-transaction transaction))
Don't forget to remove those before you go to production. You don't want to be printing everywhere.
I made a video lesson about this and other tricks for doing println
debugging in the
Repl-Driven Development in
Clojure course.
Scope is such an important topic, which is why I made a short video course called Clojure Scope all about it. In that course, we go over global, let, and dynamic scope.