Joseph Yourine
Joseph Yourine

Reputation: 1331

Clojure : implementing an "indent" function

I would like to implement a function which is a "mix " of interleave and interpose called indent.

Indeed, when you write

(interleave [1 2 3] ["_" "*"])

It returns

(1 "_" 2 "*")

But I would like to have

(1 "_" 2 "*" 3)

So I wrote a function which achieves this goal :

(defn indent
  "Mix of clojure.core interleave and interpose functions.
   Indents the second collection in the first one."
  [coll1 coll2]
  (let [n1 (count coll1)
        n2 (count coll2)
        vcoll1 (vec coll1)
        vcoll2 (vec coll2)
        stop-idx (min (- n1 2) (- n2 1))]
    (-> (loop [init '()
               i 0]
          (cond (> i stop-idx)
                  init
                :else
                  (recur (concat init [(vcoll1 i) (vcoll2 i)]) (inc i))))
        (concat [(vcoll1 (inc stop-idx))]))))

The thing is the performance is terrible :

(time (dotimes [_ 10000000] (doall f [1 2 3] ["_" "*"])))

For f = interleave : 2 s For f = indent : 7 s

I tried to mimic interleave impl but at the end I have the same costyly operations (count and vec).

The only idea I can see with fast computation is to write Java code...

Any idea ? Thanks !

EDIT : Faster java way

It's not a Clojure solution but it reduces computation time

package java_utils;

public class Clojure {

    public static Object[] indent (Object[] coll1 , Object [] coll2) {

        int len1 = coll1.length;
        int len2 = coll2.length;

    int stop_index = Math.min(len1 - 2, len2 - 1);

    Object[] result = new Object[2*(stop_index+1) + 1];

    for (int i = 0 ; i <= stop_index ; i++) {
        result[2*i] = coll1[i];
        result[2*i+1] = coll2[i];
    }

    result[2*stop_index+2] = coll1[stop_index+1];

    return result;
}

}

(defn indent
  [coll1 coll2]
  (seq (Clojure/indent (to-array coll1) (to-array coll2))))

For 10M iterations, it computes fast at 1,7s.

Upvotes: 1

Views: 85

Answers (2)

BillRobertson42
BillRobertson42

Reputation: 12883

These sorts of problems are usually best solved in Clojure by creating a seq. Typically seqs require less logic to implement, and they also fit in with the Clojure libraries well.

(defn interdent 
  ([seq1 seq2]
   (if (seq seq1)
     (lazy-seq
      (cons (first seq1) (interdent seq2 (rest seq1)))))))

Also, I'm not so sure about your time code. Not sure what f is in your example. Substituting the function in question for f produces an error. e.g.

user=> (time (dotimes [_ 10000000] (doall interpose [1 2 3] ["_" "*"])))

ArityException Wrong number of args (3) passed to: core/doall  clojure.lang.AFn.throwArity (AFn.java:429)

I used the following snippet to time the code.

user=> (time (dotimes [_ 10000000] (doall (interpose [1 2 3] ["_" "*"]))))

e.g.

user=> (interdent [1 2 3] ["_" "*"])
(1 "_" 2 "*" 3)
user=> (interdent [1 2 3] ["_" "*" ":("])
(1 "_" 2 "*" 3 ":(")
user=> (interdent [1 2 3] ["_" "*" ":)" ":("])
(1 "_" 2 "*" 3 ":)")

Upvotes: 0

leetwinski
leetwinski

Reputation: 17859

why don't you just base it on the interleave? like this:

(defn indent2 [coll1 coll2]
  (when (seq coll1)
    (cons (first coll1)
          (interleave coll2 (rest coll1)))))

it's performance should be almost equal to interleave.

Upvotes: 1

Related Questions