2
votes

I'm learning Clojure and to get a better handle on my progress I decided to start solving the Project Euler problems in the language (some of which I've already solved in C++ and Python). Problem 1 goes as follows:

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.

Here's my first run at the Clojure solution:

(defn main1
  ([n]
    (reduce +
      (filter
        #(or
          (= 0 (mod % 3))
          (= 0 (mod % 5)))
        (range n)))))

I then looked at my Python version of the code, which is as follows:

import sys
import operator
from functools import reduce


def main(n=1000):
    """
    returns solution up to but excluding n
    """
    genexp = (num for num in range(1, n) if ((num % 3 == 0) or (num % 5 == 0)))
    total = reduce(operator.add, genexp)
    return total


if __name__ == "__main__":
    if len(sys.argv) > 1:
        print(main(int(sys.argv[1])))
    else:
        print(main())

Ignoring the extra CLI-args stuff I've added in the Python version, the only major difference is that I used filter in Clojure instead of a generator. I guess I could've used a filter in Python too, but just for the sake of argument, I made my Clojure code more similar to the Python code:

(defn main2
  ([n]
    (reduce + (for [i (range n)
                  :let [div3 (= 0 (mod i 3))
                        div5 (= 0 (mod i 5))]
                  :when (or div3 div5)]
               i))))

That got me thinking - how can I benchmark these functions to compare them? For Python, it's easy enough:

$ time python python/p0001.py 10000000
23333331666668

real    0m2.693s
user    0m2.660s
sys 0m0.018s

$ time python python/p0001.py 100000000
2333333316666668

real    0m26.494s
user    0m26.381s
sys 0m0.050s

It goes up to n=100,000,000 in a reasonable amount of time (under 30 seconds). How can I run a similar test for my Clojure funcs? I imagine I'd have to compile the Clojure code first before running it. Would that even be a fair comparison, given the Python code isn't JIT-compiled here?

On another note, how idiomatic is my Clojure code (both versions)? And what are some good recommendations for styling the code? Is there a pep8-like style-guide? Or even something along the lines of a Clojure version of pep20: the "Zen of Python"?

1
"Would that even be a fair comparison?" No, there is no such thing as a fair comparison between languages. All benchmarks are flawed in some way. The best you can do is decide what you think is important about a language, and try to measure that. Do you want to measure compilation + execution time? Do you want to measure heap usage? Do you want to compare the average salary for junior engineers working with each language? - Dietrich Epp
regarding your "how idiomatic is my clojure code"? (1) unless you have a multi-arity function, it's not common to put the ( around the argument vector ([n] ... (2) (= 0 ... is often expressed as (zero? ... (3) your indentation is mostly correct but search for the clojure style guide and you'll get some good info. (4) When expressing numbers with many zeroes, consider using scientific notation. 100,000,000 in clojure is also 1e8. all in all nice job! I would second the recommendation for criterium. I use it almost daily. the 100,000,000 version shows a time of 1.84s here FYI. - Josh
FYI: This runs in about 1.1s for me: (let [n 100000000] (transduce (filter #(or (zero? (rem % 3)) (zero? (rem % 5)))) + 0 (range n))) it uses transducers to avoid building up any collection. - ClojureMostly
@DietrichEpp I guess I was looking for a naive execution-time comparison between roughly similar code in two different languages. At this early stage in my Clojure study I'm more concerned with time complexity than space complexity but I'd imagine the latter to be important too (eventually). But I take your point well. I'm drawn to Clojure because of its ease of concurrency - pmap comes to mind - and it would be nigh impossible to write "naively similar" concurrent code in Python and Clojure, what with Python's lack of focus on concurrency. - alichaudry
@Josh thanks for the tips! I actually had it set up as a multi-arity function (with the default value set to 1000 as in my Python code) but I forgot to remove the parens when I removed the default value call. The tips on using zero? and scientific notation are helpful, and github.com/bbatsov/clojure-style-guide seems like the style guide I will soon be trying to grok. - alichaudry

1 Answers

7
votes

The built-in function time is good for a first cut:

(time (main1 100000000)) => 2333333316666668
"Elapsed time: 15824.041487 msecs"

You can also use the excellent criterium library

(ns xyz.core
  (:require [criterium.core :as cc] ))

(cc/quick-bench (main1 999))

Evaluation count : 3894 in 6 samples of 649 calls.
             Execution time mean : 154.680082 µs
    Execution time std-deviation : 750.591607 ns
   Execution time lower quantile : 153.982498 µs ( 2.5%)
   Execution time upper quantile : 155.870826 µs (97.5%)
                   Overhead used : 7.898724 ns

Found 1 outliers in 6 samples (16.6667 %)
    low-severe   1 (16.6667 %)
 Variance from outliers : 13.8889 % Variance is moderately inflated by outliers