Fork me on GitHub
#beginners
<
2023-09-28
>
Matthew Twomey04:09:42

This is a purely academic question. Consider this function:

(defn test4 [url port]
  (let [driver (get-driver url :port port)]
    (when driver
      (let [view-count (find-view-count-from-video-page driver)]
        (e/quit driver)
        view-count))))
Of note, is the the driver must be “active” (not “quit”) in order to call find-view-count-from-video-page . I want the view count to be what is returned from the function, but also the driver must be shutdown before returning. I was reading an article about if let is “necessary” in Clojure, and I’m struggling to see how I could achieve this without let (if I had wanted to)?

seancorfield04:09:46

Link to the article?

seancorfield04:09:33

(let [x (f)] (g) x) can be achieved as (doto (f) (do (g))) but I consider that a "hack" and definitely not readable code. doto threads its first argument into subsequent forms, simillar to ->, but the result of each step is that original first argument value.

Matthew Twomey04:09:51

Ahh - ok, thanks Sean. I guess the term “article” I used is overstating it a bit. It was a stackoverflow discussion here: https://stackoverflow.com/questions/13616471/could-clojure-do-without-let

Matthew Twomey04:09:29

I was curious because none of the responses or discussion in there pointed to this case where you need to essentially “store” a value for use later in the function.

seancorfield04:09:20

(let [x (f)] (g) x) can be written as ((fn [x] (g) x) (f)) as a general transformation so you could argue that you don't need let, you just need (anonymous) functions. But then you could just use pure lambda calculus if you really want to get "pure" (and unreadable).

😂 1
seancorfield04:09:10

I'm surprised that someone "rarely" uses let but if you break things down into small functions and invent function names (as well as binding names) then you can "avoid" let I guess...

Matthew Twomey04:09:52

Ok yeah, makes sense. My use of Clojure for “personal tooling” has increased significantly over the past year or so and now I find I’m thinking much much more functionally. This is making me reach for some of the more advanced Clojure concepts now, hence explorations like this one. I appreciate the response 🙂

Matthew Twomey04:09:25

(personally I’m a fan of let for the clarity alone, I was just curious)

seancorfield04:09:08

It's certainly true that overuse of let can lead to some very imperative-looking code. If you find yourself adding bindings like _ (do-something to-me) in the middle of other bindings, then maybe you want to refactor into smaller functions?

1
seancorfield04:09:41

But giving names to intermediate results can often lead to much more readable code... so it's a trade off.

👍 1
andy.fingerhut04:09:00

Regarding the lambda calculus comment above, it is pretty easy to replace let in Clojure with a function call, so yeah, not strictly necessary, but I think many people find it more readable than the corresponding function call version.

1
andy.fingerhut04:09:13

Very useful when you want to compute a value once and use it multiple times without recomputing.

Matthew Twomey04:09:07

Yeah, it seems like it has a lot of utility in a lot of different ways.

andy.fingerhut04:09:58

Silly example with not-useful code body, just to show the correspondence:

andy.fingerhut04:09:00

user=> (let [x 5, y 10] (* y (+ x x y)))
200
user=> ((fn [x y] (* y (+ x x y))) 5 10)
200

andy.fingerhut04:09:19

Even the function version gives a name to the values being bound.

seancorfield04:09:35

When I got started with Clojure, I wrote pretty imperative code (despite having a long-ago background in FP) and then I went through a phase of being really, really functional in style... and I then I settled into a more pragmatic approach. Although I do quite often guilt myself for not being "functional enough" still these days 🙂

😀 1
hiredman04:09:36

In fact there are things in clojure that break when you replace a let with a lambda, so in that regard let is required

andy.fingerhut04:09:37

and enables using the value multiple times by that name, without recomputing it

andy.fingerhut04:09:13

Oh, I guess @U0NCTKEV8 you mean the sequential evaluation of let expressions?

hiredman04:09:17

Recur is the main one, you can recur across a let, but not another function

andy.fingerhut04:09:17

Or something else?

hiredman04:09:20

The other one off the top of my head is the way mutable fields for deftype scope the ability to use set!, but that is much more niche

andy.fingerhut04:09:26

recur, yeah. That is definitely something that makes the transformation let->function transformation difficult or impossible on arbitrary Clojure code.

seancorfield04:09:39

Hmm, recur, yeah... good one! What about letfn? Hard to transform to pure functions because of mutual recursion?

Matthew Twomey04:09:03

I had recur in the back of my mind too (as potentially needing let).

hiredman04:09:07

I have a macro for letfn

hiredman04:09:39

It uses let, but I don't see how it couldn't use fns

Matthew Twomey04:09:39

> Even the function version gives a name to the values being bound. Yeah, replacing let with a function, solely for the purpose of avoiding let - you’re still essentially naming and storing the value, so it’s kind of just a hack.

seancorfield04:09:25

Hmm, I suppose you could refactor the recursive calls as extra arguments and then pass the set of functions as arguments to the "body" of what was the letfn block... mechanical but tedious.

hiredman05:09:54

The CPS transform some scheme compilers do is basically that, all binding becomes function calls, and I bet a lot of them just implement let as a macro on top of lambda to begin with

seancorfield05:09:55

(letfn [(twice [x]
           (* x 2))
        (six-times [y]
           (* (twice y) 3))]
  (println "Twice 15 =" (twice 15))
  (println "Six times 15 =" (six-times 15)))
would become
((fn [twice six-times]
  (println "Twice 15 =" (twice 15))
  (println "Six times 15 =" (six-times twice 15)))
 (fn [x] (* x 2))
 (fn [f y] (* (f y) 3)))
Ugh! 😞

seancorfield05:09:11

Mind you, it's "functions all the way down" anyway so... 🤷:skin-tone-2:

hiredman05:09:03

Not that the letfn special form (letfn*?) actually uses let at all I guess, so you could get rid of let and leave letfn