August 30, 2018

Just Juxt #16: Re-implement Iterate (4clojure #62)

Iterate

Given a side-effect free function f and an initial value x write a function which returns an infinite lazy sequence of x, (f x), (f (f x)), (f (f (f x))), etc.

As usual, here is a live testing framework set up for the problem with an appropriately named placeholder function. See below for first a common "normal" solution followed by the "juxtification".

I think you're gonna like this one.

(ns live.test
  (:require [cljs.test :refer-macros [deftest is testing run-tests]]))
  
(defn spaz-out [f x]
  )

(deftest test-62
  (is (= (take 5 (spaz-out #(* 2 %) 1)) [1 2 4 8 16]))
  (is (= (take 100 (spaz-out inc 0)) (take 100 (range))))
  (is (= (take 9 (spaz-out #(inc (mod % 3)) 1)) (take 9 (cycle [1 2 3])))))
  
(run-tests)
(defn spaz-out [f x]
  (cons x
        (lazy-seq
          (spaz-out f (f x)))))
          
(run-tests)
(defn spaz-out [f x]
  (tree-seq f (juxt f) x))
  
(run-tests)

Breakdown

Part of the magic is in tree-seq, and its source is incredibly enlightening:

user=> (source tree-seq)
(defn tree-seq
  "Returns a lazy sequence of the nodes in a tree, via a depth-first walk.
   branch? must be a fn of one arg that returns true if passed a node
   that can have children (but may not).  children must be a fn of one
   arg that returns a sequence of the children. Will only be called on
   nodes for which branch? returns true. Root is the root node of the
  tree."
  {:added "1.0"
   :static true}
   [branch? children root]
   (let [walk (fn walk [node]
                (lazy-seq
                 (cons node
                  (when (branch? node)
                    (mapcat walk (children node))))))]
     (walk root)))

We can see how this is used in, for example the flatten fn:

user=> (source flatten)
(defn flatten
  "Takes any nested combination of sequential things (lists, vectors,
  etc.) and returns their contents as a single, flat sequence.
  (flatten nil) returns an empty sequence."
  {:added "1.2"
   :static true}
  [x]
  (filter (complement sequential?)
          (rest (tree-seq sequential? seq x))))

It's extremely worthwhile to spend some time digesting these. But we'll cover flatten when we get to that one.

Let's take the first test case and deconstruct it:

(take 5 (spaz-out #(* 2 %) 1))

Begin by removing (take 5 and see what happens. Just kidding.

So, we're spazzing out. Let's replace the call to spaz-out with its definition. What does it mean to spaz-out? First we can just restate it as an anonymous fn:

(take 5
        (#(tree-seq % (juxt %) %2)
    #(* 2 %) 1))

Now slurp in the args:

(take 5
      (tree-seq #(* 2 %) (juxt #(* 2 %)) 1))

Remember, tree-seq takes 3 args: branch?, children, and root. branch? is a predicate, whose result determines whether or not we call children. The root is where we start.

Tags: coding exercises KLIPSE 4clojure Cryogen juxt