Lazy Realization
Support for using realized?
with lazy sequences has landed in ClojureScript.
What kind of new fun can we have with this capability?
Fun
First, let's say we have a lazy sequence. Here's one representing the factors for each natural number:
(def factors
(map (fn [n]
(filter (fn [x]
(zero? (rem n x)))
(range 1 (inc n))))
(rest (range))))
This sequence looks like
((1) (1 2) (1 3) (1 2 4) (1 5) ...)
Even though the def
defines an infinite sequence, defining it at the REPL simply results in the var #'factors
being returned, but with none of the factors yet being calculated.
You can verify this: (realized? factors)
yields false
.
If you evaluate (first factors)
, you will get (1)
. After doing this, (realized? factors)
will yield true
which means that the first item has been realized. And you can confirm that only the first item has been realized: (realized? (rest factors))
yields false
.
Now, let's say we do something like, (take 4 factors)
, causing the next 3 items of this infinite sequence to be calculated and cached. A function like the following (due to Alan Malloy) can be used to calculate how much of the infinite sequence has been realized:
(defn realized-length [xs]
(loop [n 0 xs xs]
(if (realized? xs)
(recur (inc n) (rest xs))
n)))
With this, (realized-length factors)
produces the desired result of 4
.
Implementation
Adding support for realized?
for lazy sequences was relatively easy in ClojureScript. At its core, realized?
simply needs its argument to be of a type that satisfies cljs.core/IPending
. This involves providing an implementation for its -realized?
method.
In ClojureScript, lazy sequences are implemented by a LazySeq
deftype
with a fn
field which is used to realize the value. After this fn
has been used, it is set to nil
(it is marked as ^:mutable
), and, via this side effect, we can deduce that the value has been realized.
This strategy is in fact analogous to the one used in Delay
—the type that underlies the delay
function—and its existing support for realized?
in ClojureScript.
The implementation landed for realized?
for LazySeq
simply mimics the approach used for Delay
, checking to see if the fn
mutable field has been set to nil
in the inline IPending
implementation in the LazySeq
deftype
:
IPending
(-realized? [x]
(not fn))
That's all there is to it!