Just Juxt #16: Re-implement Iterate (4clojure #62)
Given a side-effect free function
f
and an initial valuex
write a function which returns an infinite lazy sequence ofx
,(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.