1
votes

I am playing around and trying to create my own reductions implementation, so far I have this which works with this test data:

((fn [func & args]
  (reduce (fn [acc item]
            (conj acc (func (last acc) item))
            )[(first args)] (first (rest args)))) * 2 [3 4 5]

What I don't like is how I am separating the args.

(first args) is what I would expect, i.e. 2 but (rest args) is ([3 4 5]) and so I am getting the remainder like this (first (rest args)) which I do not like.

Am I missing some trick that makes it easier to work with variadic arguments?

2
Note you can use second: (second [3 4 5]) is 4. - bfontaine
You may find interesting this blog. - Mark Karpov

2 Answers

3
votes

Variadic arguments are just about getting an unspecified number of arguments in a list, so all list/destructuring operations can be applied here.

For example:

(let [[fst & rst] a-list]
  ; fst is the first element
  ; rst is the rest
 )

This is more readable than:

(let [fst (first a-list)
      rst (rest a-list)]
  ; ...
 )

You can go further to get the first and second elements of a list (assuming it has >1 elements) in one line:

(let [fst snd & rst]
  ; ...
 )

I originally misread your question and thought you were trying to reimplement the reduce function. Here is a sample implementation I wrote for this answer which does’t use first or rest:

(defn myreduce
  ;; here we accept the form with no initial value
  ;; like in (myreduce * [2 3 4 5]), which is equivalent
  ;; to (myreduce * 2 [3 4 5]). Notice how we use destructuring
  ;; to get the first/rest of the list passed as a second
  ;; argument
  ([op [fst & rst]] (myreduce op fst rst))
  ;; we take an operator (function), accumulator and list of elements
  ([op acc els]
    ;; no elements? give the accumulator back
    (if (empty? els)
      acc
      ;; all the function's logic is in here
      ;; we're destructuring els to get its first (el) and rest (els)
      (let [[el & els] els]
        ;; then apply again the function on the same operator,
        ;; using (op acc el) as the new accumulator, and the
        ;; rest of the previous elements list as the new
        ;; elements list
        (recur op (op acc el) els)))))

I hope it helps you see how to work with list destructuring, which is probably what you want in your function. Here is a relevant blog post on this subject.

1
votes

Tidying up your function.

As @bfontaine commented, you can use (second args) instead of (first (rest args)):

(defn reductions [func & args]
  (reduce
    (fn [acc item] (conj acc (func (last acc) item)))
    [(first args)]
    (second args)))

This uses

  • func
  • (first args)
  • (second args)

... but ignores the rest of args.

So we can use destructuring to name the first and second elements of args - init and coll seem suitable - giving

(defn reductions [func & [init coll & _]]
  (reduce
    (fn [acc item] (conj acc (func (last acc) item)))
    [init]
    coll))

... where _ is the conventional name for the ignored argument, in this case a sequence.

We can get rid of it, simplifying to

(defn reductions [func & [init coll]] ... )

... and then to

(defn reductions [func init coll] ... )

... - a straightforward function of three arguments.

Dealing with the underlying problems.

Your function has two problems:

  • slowness
  • lack of laziness.

Slowness

The flashing red light in this function is the use of last in

(fn [acc item] (conj acc (func (last acc) item)))

This scans the whole of acc every time it is called, even if acc is a vector. So this reductions takes time proportional to the square of the length of coll: hopelessly slow for long sequences.

A simple fix is to replace (last acc) by (acc (dec (count acc))), which takes effectively constant time.

Lack of laziness

We still can't lazily use what the function produces. For example, it would be nice to encapsulate the sequence of factorials like this:

(def factorials (reductions * 1N (next (range)))))

With your reductions, this definition never returns.

You have to entirely recast your function to make it lazy. Let's modify the standard -lazy -reductions to employ destructuring:

(defn reductions [f init coll]
  (cons
    init
    (lazy-seq
      (when-let [[x & xs] (seq coll)]
        (reductions f (f init x) xs)))))

Now we can define

(def factorials (reductions * 1N (next (range))))

Then, for example,

(take 10 factorials)
;(1N 1N 2N 6N 24N 120N 720N 5040N 40320N 362880N)

Another approach is to derive the sequence from itself, like a railway locomotive laying the track it travels on:

(defn reductions [f init coll]
  (let [answer (lazy-seq (reductions f init coll))]
    (cons init (map f answer coll))))

But this contains a hidden recursion (hidden from me, at least):

(nth (reductions * 1N (next (range))) 10000)
;StackOverflowError ...