Functional alternative to "let"

Viewed 208

I find myself writing a lot of clojure in this manner:

(defn my-fun [input]
  (let [result1 (some-complicated-procedure input) 
        result2 (some-other-procedure result1)]
    (do-something-with-results result1 result2)))

This let statement seems very... imperative. Which I don't like. In principal, I could be writing the same function like this:

(defn my-fun [input]
    (do-something-with-results (some-complicated-procedure input) 
                               (some-other-procedure (some-complicated-procedure input)))))

The problem with this is that it involves recomputation of some-complicated-procedure, which may be arbitrarily expensive. Also you can imagine that some-complicated-procedure is actually a series of nested function calls, and then I either have to write a whole new function, or risk that changes in the first invocation don't get applied to the second:

E.g. this works, but I have to have an extra shallow, top-level function that makes it hard to do a mental stack trace:

(defn some-complicated-procedure [input] (lots (of (nested (operations input))))) 
(defn my-fun [input]
    (do-something-with-results (some-complicated-procedure input) 
                               (some-other-procedure (some-complicated-procedure input)))))

E.g. this is dangerous because refactoring is hard:

(defn my-fun [input]
    (do-something-with-results (lots (of (nested (operations (mistake input))))) ; oops made a change here that wasn't applied to the other nested calls
                               (some-other-procedure (lots (of (nested (operations input))))))))

Given these tradeoffs, I feel like I don't have any alternatives to writing long, imperative let statements, but when I do, I cant shake the feeling that I'm not writing idiomatic clojure. Is there a way I can address the computation and code cleanliness problems raised above and write idiomatic clojure? Are imperitive-ish let statements idiomatic?

6 Answers

The kind of let statements you describe might remind you of imperative code, but there is nothing imperative about them. Haskell has similar statements for binding names to values within bodies, too.

If your situation really needs a bigger hammer, there are some bigger hammers that you can either use or take for inspiration. The following two libraries offer some kind of binding form (akin to let) with a localized memoization of results, so as to perform only the necessary steps and reuse their results if needed again: Plumatic Plumbing, specifically the Graph part; and Zach Tellman's Manifold, whose let-flow form furthermore orchestrates asynchronous steps to wait for the necessary inputs to become available, and to run in parallel when possible. Even if you decide to maintain your present course, their docs make good reading, and the code of Manifold itself is educational.

I recently had this same question when I looked at this code I wrote

(let [user-symbols (map :symbol states)
      duplicates (for [[id freq] (frequencies user-symbols) :when (> freq 1)] id)]
  (do-something-with duplicates))

You'll note that map and for are lazy and will not be executed until do-something-with is executed. It's also possible that not all (or even not any) of the states will be mapped or the frequencies calculated. It depends on what do-something-with actually requests of the sequence returned by for. This is very much functional and idiomatic functional programming.

i guess the simplest approach to keep it functional would be to have a pass-through state to accumulate the intermediate results. something like this:

(defn with-state [res-key f state]
  (assoc state res-key (f state)))

user> (with-state :res (comp inc :init) {:init 10})
;;=> {:init 10, :res 11}

so you can move on to something like this:

(->> {:init 100}
     (with-state :inc'd (comp inc :init))
     (with-state :inc-doubled (comp (partial * 2) :inc'd))
     (with-state :inc-doubled-squared (comp #(* % %) :inc-doubled))
     (with-state :summarized (fn [st] (apply + (vals st)))))

;;=> {:init 100,
;;    :inc'd 101,
;;    :inc-doubled 202,
;;    :inc-doubled-squared 40804,
;;    :summarized 41207}

The let form is a perfectly functional construct and can be seen as syntactic sugar for calls to anonymous functions. We can easily write a recursive macro to implement our own version of let:

(defmacro my-let [bindings body]
  (if (empty? bindings)
    body
    `((fn [~(first bindings)]
        (my-let ~(rest (rest bindings)) ~body))
      ~(second bindings))))

Here is an example of calling it:

(my-let [a 3
         b (+ a 1)]
        (* a b))
;; => 12

And here is a macroexpand-all called on the above expression, that reveal how we implement my-let using anonymous functions:

(clojure.walk/macroexpand-all '(my-let [a 3
                                        b (+ a 1)]
                                       (* a b)))
;; => ((fn* ([a] ((fn* ([b] (* a b))) (+ a 1)))) 3)

Note that the expansion doesn't rely on let and that the bound symbols become parameter names in the anonymous functions.

As others write, let is actually perfectly functional, but at times it can feel imperative. It's better to become fully comfortable with it.

You might, however, want to kick the tires of my little library tl;dr that lets you write code like for example

(compute 
     (+ a b c)
 where
     a (f b)
     c (+ 100 b))
Related