Skip to content

Instantly share code, notes, and snippets.

@jwieringa
Last active December 30, 2015 09:49
Show Gist options
  • Select an option

  • Save jwieringa/7812094 to your computer and use it in GitHub Desktop.

Select an option

Save jwieringa/7812094 to your computer and use it in GitHub Desktop.
How do I learn what idiomatic Clojure looks like?
;Multiples of 3 and 5
; If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.
; Find the sum of all the multiples of 3 or 5 below 1000.
; Solve using seqs
; My personal favorite
; I like it because I think it is using lazy seqs which, if I understand things, is a great
; way to solve this problem in clojure. It does bother me that there are numbers being generated
; that are being dropped. i.e. is there a way to process this so that the union can be dropped?
(defn find-and-sum-multiples [m1 m2 limit]
"Returns the sum of multiples of m1 and m2 upto limit"
(reduce + (clojure.set/union (set (range 0 limit m1))
(set (range 0 limit m2)))))
(find-and-sum-multiples 3 5 1000)
; Solve Recursively
(defn multiple-3-or-5 [n]
(cond (= 0 (rem n 5)) n
(= 0 (rem n 3)) n
:else 0))
(defn recur-multiples-of-3-5 [limit]
(loop [limit limit
acc 0]
(if (= limit 0)
acc
(recur (dec limit) (+ acc (multiple-3-or-5 limit))))))
(recur-multiples-of-3-5 1000)
@jwieringa
Copy link
Author

And then I find this:

(->> [3 5]
     (map (partial range 0 1000))
     flatten
     set
     (reduce +))

Note: While this demonstrates the coolness of macros, I'm not sure it is a good use of one. Current reasoning, if a macro isn't necessary, then avoid using it. In this case, a macro isn't necessary and I'm not sure it brings any clarity to what is going on. I do wonder if it has a better benchmark.

@jwieringa
Copy link
Author

Benchmarks

Summary: My assumption was that find-and-sum-multiples was going to be the most efficient because it utilizes lazy sequencing. I think that these benchmarks verify that assumption, but I think there may be some optimizations in find-and-sum-multiples that the others are not able to take advantage of (i.e. once the function is run once do the solutions stick around?).

While benchmarking perhaps isn't the best way to discover idiomatic Clojure, it seem to provide some insight into how the individual bricks are behaving.

Benchmarking with: (use 'criterium.core)

Using seqs:

user=> (defn find-and-sum-multiples [m1 m2 limit]
  #_=>   "Returns the sum of multiples of m1 and m2 upto limit"
  #_=>   (reduce + (clojure.set/union (set (range 0 limit m1))
  #_=>                                (set (range 0 limit m2)))))
#'user/find-and-sum-multiples
user=> (bench (find-and-sum-multiples 100 3 5))
WARNING: Final GC required 2.1605409926290777 % of runtime
Evaluation count : 49463460 in 60 samples of 824391 calls.
             Execution time mean : 1.215210 µs
    Execution time std-deviation : 98.359408 ns
   Execution time lower quantile : 1.128250 µs ( 2.5%)
   Execution time upper quantile : 1.429801 µs (97.5%)
                   Overhead used : 2.709801 ns

Found 3 outliers in 60 samples (5.0000 %)
    low-severe   2 (3.3333 %)
    low-mild     1 (1.6667 %)
 Variance from outliers : 60.1551 % Variance is severely inflated by outliers
nil

Using Recursion:

user=> (defn multiple-3-or-5? [n]
  #_=>   (cond (= 0 (rem n 5)) n
  #_=>         (= 0 (rem n 3)) n
  #_=>         :else 0))
#'user/multiple-3-or-5?
user=> (defn recur-multiples-of-3-5 [limit]
  #_=>   (loop [limit limit
  #_=>          acc 0]
  #_=>     (if (= limit 0)
  #_=>       acc
  #_=>       (recur (dec limit) (+ acc (multiple-3-or-5? limit))))))
#'user/recur-multiples-of-3-5
user=> (bench (recur-multiples-of-3-5 100))
Evaluation count : 7547640 in 60 samples of 125794 calls.
             Execution time mean : 7.914505 µs
    Execution time std-deviation : 321.042089 ns
   Execution time lower quantile : 7.612677 µs ( 2.5%)
   Execution time upper quantile : 8.621702 µs (97.5%)
                   Overhead used : 2.709801 ns

Found 5 outliers in 60 samples (8.3333 %)
    low-severe   3 (5.0000 %)
    low-mild     2 (3.3333 %)
 Variance from outliers : 27.0628 % Variance is moderately inflated by outliers
nil

Using Macro:

This result surprised me. I suspect that there is a way to build this macro such that it behaves more efficiently.

user=> (bench (->> [3 5]
  #_=>      (map (partial range 0 100))
  #_=>      flatten
  #_=>      set
  #_=>      (reduce +))
  #_=> )
Evaluation count : 1150500 in 60 samples of 19175 calls.
             Execution time mean : 53.613235 µs
    Execution time std-deviation : 828.676554 ns
   Execution time lower quantile : 52.091475 µs ( 2.5%)
   Execution time upper quantile : 54.968066 µs (97.5%)
                   Overhead used : 2.709801 ns
nil

@jwieringa
Copy link
Author

Adding another one to the mix after reading about a patter in The Joy of Clojure.

(defn multiple? [n]
  (or (zero? (rem n 5))
      (zero? (rem n 3))))

(reduce +
        (for [x (range 1 100) :when (multiple? x)]
          x))

Benchmarked:

user=> (defn multiple? [n]
  #_=>   (or (zero? (rem n 5))
  #_=>       (zero? (rem n 3))))
#'user/multiple?
user=> (bench (reduce + (for [x (range 1 100) :when (multiple? x)] x)))
WARNING: Final GC required 2.136619650272382 % of runtime
Evaluation count : 6217140 in 60 samples of 103619 calls.
             Execution time mean : 10.131764 µs
    Execution time std-deviation : 349.525571 ns
   Execution time lower quantile : 9.619484 µs ( 2.5%)
   Execution time upper quantile : 10.970652 µs (97.5%)
                   Overhead used : 2.747116 ns

Found 2 outliers in 60 samples (3.3333 %)
    low-severe   2 (3.3333 %)
 Variance from outliers : 20.6394 % Variance is moderately inflated by outliers
nil

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment