D. K.
D. K.

Reputation: 61

Clojure loop inside let (global v local variable)

I was writing the code that does same thing as 'reduce' function in clojure ex) (reduce + [1 2 3 4]) = (+ (+ (+ 1 2) 3) 4).

(defn new-reduce [fn coll]
  (def answer (get coll 0))
  (loop [i 1]
    (when (< i (count coll))
      (def answer (fn answer (get coll i)))
    (recur (inc i))))
answer)

In my code I used the global variable, and for me it was easier for me to understand that way. Apparently, people saying it is better to change the global variable to local variable such as let. So I tried..

(defn new-reduce [fn coll]
  (let [answer (get coll 0)] 
   (loop [i 1]
     (when (< i (count coll))
      (fn answer (get coll i))
     (recur (inc i))))))

To be honest, I am not really familiar with let function and even though I try really simple code, it did not work. Can somebody help me to fix this code and help me to understand how the let (local variables) really work ? Thank you. (p.s. really simple code that has loop inside let function will be great also).

Upvotes: 1

Views: 874

Answers (2)

Arthur Ulfeldt
Arthur Ulfeldt

Reputation: 91617

Let does not create local "variables", it gives names to values, and does not let you change them after giving them the name. So introducing a let is more like defining a local constant.

First I'll just add another item into the loop expression to store the value so far. Each time through the loop we will update this to incorporate the new information. This pattern is very common. I also needed to add a new argument to the function to hold the initial state (reduce as a concept needs this)

user> (defn new-reduce [function initial-value coll]
        (loop [i 0 
               answer-so-far initial-value]
          (if (< i (count coll))
            (recur (inc i) (function answer-so-far (get coll i)))
            answer-so-far)))

user> (new-reduce + 0 [1 2 3])
6

This moves the "global variable" into a name that is local to the loop expression can be updated once per loop at the time you jump back up to the top. Once it reaches the end of the loop it will return the answer thus far as the return value of the function rather than recurring again. Building your own reduce function is a great way to build understanding on how to use reduce effectively.

There is a function that introduces true local variables, though it is very nearly never used in Clojure code. It's only really used in the runtime bootstap code. If you are really curious read up on binding very carefully.

Upvotes: 1

Sam Estep
Sam Estep

Reputation: 13354

Here's a simple, functional solution that replicates the behavior of the standard reduce:

(defn reduce
  ([f [head & tail :as coll]]
   (if (empty? coll)
     (f)
     (reduce f head tail)))
  ([f init [head & tail :as coll]]
   (cond
     (reduced? init) @init
     (empty? coll) init
     :else (recur f (f init head) tail))))

There is no loop here, because the function itself serves as the recursion point. I personally find it easier to think about this recursively, but since we're using tail recursion with recur, you can think about it imperatively/iteratively as well:

  1. If init is a signal to return early then return its value, otherwise go to step 2
  2. If coll is empty then return init, otherwise go to step 3
  3. Set init to the result of calling f with init and the first item of coll as arguments
  4. Set coll to a sequence of all items in coll except the first one
  5. Go to step 1

Actually, under the hood (with tail-call optimization and such), that's essentially what's really going on. I'd encourage you to compare these two expressions of the same solution to get a better idea of how to go about solving these sorts of problems in Clojure.

Upvotes: 0

Related Questions