maxorcist
maxorcist

Reputation: 369

clojure for function resets let

I am trying to solve a problem on 4clojure.com where I'm supposed to count the number of elements in a collection without using count. I have tried two ways using for and let that I feel should work but it seems that the for-loop keeps resetting the let.

(#(for [x %  :let [y 0]] (inc y)) [1 2 3 4 5])
;; which returns
(1 1 1 1 1)

(#(let [y 0] (for [x %] (inc y))) [1 2 3 4 5])
;; which returns
(1 1 1 1 1)

So my question is why this happens, and how I get my "variable" to keep incrementing for each item in the collection. Just saying the word variable makes me wonder if I'm trying to make something muteable that can't, but I still feel like this should work.

Upvotes: 3

Views: 218

Answers (1)

leetwinski
leetwinski

Reputation: 17859

in both cases you can't alter the value of y:

in first case for re-introduces the y on every loop step, that's why you couldn't change it's value, even if it was mutable.

the second case shows that the value is truly immutable, giving you (inc y) on every step, but y is always zero. Simple example:

(let [x 10]
  (inc x)
  x)

;;=> 10 

In general this kind of task can usually be solved with the following approaches:

First one is the simple recursion:

(defn count-rec [data]
  (if (seq data)
    (inc (count-rec (rest data)))
    0))

user> (count-rec [1 2 3 4 5])
;;=> 5

it is flawed in a way, since it is not tail recursive, and would fail for large collections

The second one is clojure's loop:

(defn count-loop [data]
  (loop [res 0 data data]
    (if (seq data)
      (recur (inc res) (rest data))
      res)))

user> (count-loop [1 2 3 4 5])
;;=> 5

also you can use explicit tail recursion which would be very alike:

(defn count-tailrec
  ([data] (count-tailrec 0 data))
  ([c data] (if (seq data)
              (recur (inc c) (rest data))
              c)))

user> (count-tailrec [1 2 3 4 5])
;;=> 5

The third one would use the reduce function:

(defn count-reduce [data]
  (reduce (fn [res _] (inc res)) 0 data))

user> (count-reduce [1 2 3 4 5])
;;=> 5

Just for fun you could also use this way (i would not advice it, since it is an overkill in comparison with reduce):

(defn count-map [data]
  (apply + (map (constantly 1) data)))

user> (count-map [1 2 3 4 5])
;;=> 5

You could also use clojure's mutable primitives, but it is not idiomatic, and should be avoided for as long as it could be:

(defn count-atom [data]
  (let [c (atom 0)]
    (run! (fn [_] (swap! c inc)) data)
    @c))

user> (count-atom [1 2 3 4 5])
;;=> 5

and here comes the cheat [SPOILER ALERT!]

4clojure blocks the usage of count function in this task, but doesn't block the .size method of java's collections, so it could be solved with #(.size (seq %))

Upvotes: 10

Related Questions