1
votes

I read a lot of documentation about Clojure (and shall need to read it again) and read several Clojure questions here on SO to get a "feel" of the language. Besides a few tiny functions in elisp I've never written in any Lisp language before. I wrote my first project Euler solution in Clojure and before going further I'd like to better understand something about map and reduce.

Using a lambda, I ended up with the following (to sum all multiple of either 3 or 5 or both between 1 and 1000 inclusive):

(reduce + (map #(if (or (= 0 (mod %1 3)) (= 0 (mod %1 5))) %1 0) (range 1 1000)))

I put it on one line because I wrote it on the REPL (and it gives the correct solution).

Without the lambda, I wrote this:

(defn val [x] (if (or (= 0 (mod x 3)) (= 0 (mod x 5))) x 0))

And then I compute the solution doing this:

(reduce + (map val (range 1 1000)))

In both cases, my question concerns what the map should return, before doing the reduce. After doing the map I noticed I ended up with a list looking like this: (0 0 3 0 5 6 ...).

I tried removing the '0' at the end of the val definition but then I received a list made of (nil nil 3 nil 5 6 etc.). I don't know if the nil are an issue or not. I figured out that I was going to sum while doing a fold-left anyway so that the zero weren't really an issue.

But still: what's a sensible map to return? (0 0 3 0 5 6 ...) or (nil nil 3 nil 5 6...) or (3 5 6 ...) (how would I go about this last one?) or something else?

Should I "filter out" the zeroes / nils and if so how?

I know I'm asking a basic question but map/reduce is obviously something I'll be using a lot so any help is welcome.

3

3 Answers

2
votes

It sounds like you already have an intuative undestanding of the need to seperate mapping concerns form the reducing It's perfectly natural to have data produced by map that is not used by the reduce. infact using the fact that zero is the identity value for addition make this even more elegant.

  • mappings job is to produce the new data (in this case 3 5 or "ignore")
  • reduces job is to decide what to include and to produce the final result.

what you started with is idiomatic clojure and there is no need to complicate it any more, so this next example is just to illustrate the point of having map decide what to include:

(reduce #(if-not (zero? %1) (+ %1 %2) %2) (map val (range 10)))

in this contrived example the reduce function ignores the zeros. In typical real world code if the idea was as simple as filtering out some value then people tend to just use the filter function

(reduce + (filter #(not (zero? %)) (map val (range 10))))

you can also just start with filter and skip the map:

(reduce + (filter #(or (zero? (rem % 3)) (zero? (rem % 5))) (range 10)))
1
votes

The watchword is clarity.

  • Use filter, not map. Then you don't have to choose a null value that you later have to decide not to act on.
  • Naming the filtering/mapping function can help. Do so with let or letfn, not defn, unless you have use for the function elsewhere.

Acting on this advice brings us to ...

(let [divides-by-3-or-5? (fn [n] (or (zero? (mod n 3)) (zero? (mod n 5))))]
  (reduce + (filter divides-by-3-or-5? (range 1 1000))))

You may want to stop here for now.

This reads well, but the divides-by-3-or-5? function sticks in the throat. Change the factors and we need a completely new function. And that repeated phrase (zero? (mod n ...)) jars. So ...

We want a function, that - given a list (or other collection) of possible factors - tells us whether any of them apply to a given number. In other words, we want

  • a function of a collection of numbers - the possible factors - ...
  • that returns a function of one number - the candidate - ...
  • that tells us whether the candidate is divisible by any of the possible factors.

One such function is

(fn [ns] (fn [n] (some (fn [x] (zero? (mod n x))) ns)))

... which we can employ thus

(let [divides-by-any? (fn [ns] (fn [n] (some (fn [x] (zero? (mod n x))) ns)))]
  (reduce + (filter (divides-by-any? [3 5]) (range 1 1000))))

Notes

  • This "improvement" has made the program a little slower.
  • divides-by-any? might prove useful enough to be promoted to a defn.
  • If the operation were critical, you could consider stripping out redundant factors. For example [2 3 6] could be reduced to [6].
  • If the operation were really critical, and the factors were supplied as constants, you could consider creating the filter function with a macro that went back to using or.

This is a bit of a shaggy-dog story, but it recounts the thoughts prompted by the problem you refer to.

0
votes

In your case I would use keep instead of map. It is similar to map except that it keeps only the non-nil values.