bobbicodes / bobbi-lisp

Interactive Lisp environment for learning Clojure
https://bobbicodes.github.io/bobbi-lisp/
0 stars 0 forks source link

Map on arbitrary number of collections #28

Closed bobbicodes closed 11 months ago

bobbicodes commented 11 months ago

Fixes: #26

The challenge here is that we don't have lazy seqs, so map is just an imperative loop:

(def map
  (fn* 
   ([f coll]
    (loop* [s (seq coll) res []]
      (if (empty? s) (apply list res)
          (recur (rest s)
                 (conj res (if (keyword? f) (get (first s) f) (f (first s))))))))
   ([f c1 c2]
    (loop* [s1 (seq c1) s2 (seq c2) res []]
      (if (or (empty? s1) (empty? s2)) (apply list res)
          (recur (rest s1) (rest s2)
                 (conj res (f (first s1) (first s2)))))))
...)

So we needed a way of "tying the knot" to make it work on any number of collections.

Here is how it is done in Clojure:

([f c1 c2 c3 & colls]
   (let [step (fn step [cs]
                 (lazy-seq
                  (let [ss (map seq cs)]
                    (when (every? identity ss)
                      (cons (map first ss) (step (map rest ss)))))))]
     (map #(apply f %) (step (conj colls c3 c2 c1))))

I tried unsuccessfully to translate it into a loop, and eventually realized that I don't think that can work in a non-lazy context. But I knew exactly what code would work if it could be generated for any number of arguments. Just like the other arities, only bigger.

So, I wrote my first macro (first one without help anyway):

(defmacro map+ [f colls]
  (let [n (range (count colls))
        loop-keys (conj (mapv #(symbol (str "s" %)) n) 'res)
        loop-vals (conj (mapv #(list 'seq (list 'nth colls %)) n) [])
        ors (cons 'or (map #(list 'empty? (symbol (str "s" %))) n))
        recur1 (cons 'recur (map #(list 'rest (symbol (str "s" %))) n))
        recur2 (list 'conj 'res (cons 'f (map #(list 'first (symbol (str "s" %))) n)))]
    `(loop ~(vec (interleave loop-keys loop-vals))
       (if ~ors (apply list res)
           ~(concat recur1 (list recur2))))))

Then, it gets called by the variable arity of map:

([f c0 c1 c2 & colls]
    (map+ f (list* c1 c2 c3 colls)))

Example mapping on 5 collections:

(map str [1 2] [3 4] [5 6] [7 8] [9 0])
=> 
("13579" "24680")