user524824
user524824

Reputation:

Clojure: Are side-effect-free calculations executed many times

lets say I have a side-effect free function that I use repeatedly with the same parameters without storing the results in a variable. Does Clojure notice this and uses the pre-calculated value of the function or is the value recalculated all the time?

Example:

(defn rank-selection [population fitness]
  (map
    #(select-with-probability (sort-by fitness population) %)
    (repeatedly (count population) #(rand))))

(defn rank-selection [population fitness]
  (let [sorted-population (sort-by fitness population)]
    (map
      #(select-with-probability sorted-population %)
      (repeatedly (count population) #(rand)))))

In the first version sort-by is executed n-times (where n is the size of the population). In the second version sort-by is executed once and the result is used n-times

Does Clojure stores the result nonetheless? Are these methods comparable fast?

Upvotes: 2

Views: 174

Answers (2)

juan.facorro
juan.facorro

Reputation: 9930

Clojure doesn't store the results unless you specify that in your code, either by using memoize like mentioned in the comments or by saving the calculation/result in a local binding like you did.

Regarding the questions about how fast is one function regarding the other, here's some code that returns the time for the execution of each (I had to mock the select-with-probability function). The doalls are necessary to force the evaluation of the result of map.

(defn select-with-probability [x p]
  (when (< p 0.5)
    x))

(defn rank-selection [population fitness]
  (map
    #(select-with-probability (sort-by fitness population) %)
    (repeatedly (count population) rand)))

(defn rank-selection-let [population fitness]
  (let [sorted-population (sort-by fitness population)]
    (map
      #(select-with-probability sorted-population %)
      (repeatedly (count population) rand))))
      
(let [population (take 1000 (repeatedly #(rand-int 10)))]
  (time (doall (rank-selection population <)))
  (time (doall (rank-selection-let population <)))
  ;; So that we don't get the result seq
  nil)

This returns the following in my local environment:

"Elapsed time: 95.700138 msecs"
"Elapsed time: 1.477563 msecs"
nil

EDIT

In order to avoid the use of the let form you could also use partial which receives a function and any number of arguments, and returns a partial application of that function with the values of the arguments supplied. The performance of the resulting code is in the same order as the one with the let form but is more succinct and readable.

(defn rank-selection-partial [population fitness]
  (map
    (partial select-with-probability (sort-by fitness population))
    (repeatedly (count population) rand)))

(let [population (take 1000 (repeatedly #(rand-int 10)))]
  (time (doall (rank-selection-partial population <)))  
  ;; So that we don't get the result seq
  nil)

;= "Elapsed time: 0.964413 msecs"

Upvotes: 1

Nathan Hughes
Nathan Hughes

Reputation: 96394

In Clojure sequences are lazy, but the rest of the language, including function evaluation, is eager. Clojure will invoke the function every time for you. Use the second version of your rank-selection function.

Upvotes: 1

Related Questions