Named argument version of range

Clojure’s range function supports the following parameter options:

([] [end] [start end] [start end step])

But what if you want to specify an end but not a start?  Or a step but no end?

As a starting point, we can make a minor variant of range that lets you supply a start, or start and step, without an end:

(defn range-start
"Return a lazy seq like range but with single param
indicating start of range, two params indicating
start and step "
([start] (range start Double/POSITIVE_INFINITY 1))
([start step] (range start Double/POSITIVE_INFINITY step)))

For completeness we could support no arguments as well, but the behavior would be identical to range. Let’s try this one as-is.  Note that without an “end” value you get an infinite sequence – so we’ll use take to get a finite subset:

user> (take 10 (range-start 5))
(5 6 7 8 9 10 11 12 13 14)
user> (take 10 (range-start 5 2))
(5 7 9 11 13 15 17 19 21 23)

So, that’s marginally useful, but still lacking the ability, e.g. to specify only a step.  So lets move on to the complete solution. One approach is to use named arguments so that you can choose any combination of arguments (in any order). Calling the function would look like this for example:

user> (narg-range :e 10 :s 3)
(0 3 6 9)

The code to implement this behavior is quite simple.

(defn narg-range
"Named argument version of range, :b begin :e end :s step.
Defaults are :b 0 :e infinity :s 1. All parameters are optional."
[& {:keys [b e s]
    :or {b 0 e Double/POSITIVE_INFINITY s 1}}]
(range b e s))

Of particular interest is the argument list specifier “& {:keys [b e s]}”.  “&{ }” specifies that the argument list should be treated as a map.  “:keys” tells Clojure that the vector [b e s] should be converted to keywords. The keywords are then looked up in the map and assigned to the matching variables. The “:or” specifies defaults for any value not specified.
So let’s give it a spin!

user=> (take 3 (narg-range))
(0 1 2)
user=> (narg-range :e 15)
(0 1 2 3 4 5 6 7 8 9 10 11 12 13 14)
user=> (take 20 (narg-range :b 5))
(5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24)
user=> (take 20 (narg-range :b 5 :s 3))
(5 8 11 14 17 20 23 26 29 32 35 38 41 44 47 50 53 56 59 62)
user=> (narg-range :e 15 :s 2)
(0 2 4 6 8 10 12 14)
user=> (narg-range :e 15 :b 2)
(2 3 4 5 6 7 8 9 10 11 12 13 14)

There you have it. Handy.

fin!

Clojure Pi

This is a beginner’s example to calculate Pi using Clojure sequences and a few commonly used functions like range, repeat, flatten, map, and reduce.

There is a series expansion for Pi which looks like this:

π/4 = 2/3 * 4/3 * 4/5 * 6/5 * 6/7 * 8/7 * 8/9 * …

As you can see there are patterns in the sequence of numerators 2,4,4,6,6,8,8.. and denominators, 3,3,5,5,7,7,9,9..

Clojure sequences are nicely suited to creating and processing these sequences to calculate π using this expansion.

We’ll create the denominator sequence first since it’s slightly simpler. Ignoring the repeats for now, we can see that we have a pattern 3, 5, 7… which can be generated easily by the range function, which can take optional arguments for start, end, and step:

user=> (range 3 20 2)
(3 5 7 9 11 13 15 17 19)

Now we’d like to repeat each element twice.  We apply the repeat function with a count of 2 to each element using map.  Note that repeat is applied using an anonymous function with the syntax shorthand #( ) . map will take the first sequence element produced by range and pass it to the anonymous function as a parameter in place of the %.

user=> (map #(repeat 2 %) (range 3 20 2))
((3 3) (5 5) (7 7) (9 9) (11 11) (13 13) (15 15) (17 17) (19 19))

That’s close but we really want a sequence of single items, not pairs. That can be fixed with flatten:

user=> (flatten (map #(repeat 2 %) (range 3 20 2)))
(3 3 5 5 7 7 9 9 11 11 13 13 15 15 17 17 19 19)

Great. The denominator sequence is done. The numerator sequence is similar, but starts at 4 instead of 3. We also need to add a 2 onto the start of the sequence with cons

user=> (cons 2 (flatten (map #(repeat 2 %) (range 4 20 2))))
(2 4 4 6 6 8 8 10 10 12 12 14 14 16 16 18 18)

Perfect. Just to make the code a bit cleaner, Let’s define variables for each sequence

user=> (def numerators (cons 2 (flatten (map #(repeat 2 %) (range 4 20 2)))))
#'user/numerators
user=> (def denominators (flatten (map #(repeat 2 %) (range 3 50 2))))
#'user/denominators

The next step is to calculate the product of all the fractions. We could choose to either multiply first or divide first. I’m going to divide because I like seeing the resulting sequence of fractions. We do this by mapping the function “/” onto the two sequences. Map takes an arbitrary number of sequences as arguments, takes the first element from each sequence and applies the specified function, then takes the next element from each sequence and so on.

user=> (map / numerators denominators)
(2/3 4/3 4/5 6/5 6/7 8/7 8/9 10/9 10/11 12/11 12/13 14/13 14/15 16/15 16/17 18/17 18/19)

Almost done. Now multiply all those fractions together with reduce, which calls a function with the first 2 elements of a sequence, then calls the function with that result plus the 3rd element of the sequence, and so on until the sequence is consumed.  When used with “*” it multiplies all the elements of the sequence together.

user=> (reduce * (map / numerators denominators))
2147483648/2807136475

And finally, since the product of the fractions is π/4 we multiply by 4 to get π.  Using 4.0 instead of 4 will force the result to be expressed as a decimal instead of a fraction.

user=> (* 4.0 (reduce * (map / numerators denominators)))
3.0600345471268904

Ok, that seems to be in the ballpark.  The poor approximation is because we used so few terms in our sequences. By using more terms (ending the range at 1000 instead of 20) we get a bit closer:

user=> (def numerators (cons 2 (flatten (map #(repeat 2 %) (range 4 1000 2)))))
#'user/numerators
user=> (def denominators (flatten (map #(repeat 2 %) (range 3 1000 2))))
#'user/denominators
user=> (* 4.0 (reduce * (map / numerators denominators)))
3.1400206785767786

Much better. In any case, I hope this example showed off how a few of Clojure’s features and functions can be combined easily to solve interesting problems.  If you have thoughts about how to improve this example, let me know!

Thanks!