io.github.frenchy64.fully-satisfies.safe-locals-clearing
Provides safer versions of delay and lazy-seq that ensure the
function calculating their cached values is only called once
while preserving their locals-clearing capabilities.
The tradeoff being they now cannot be recursively dereferenced,
instead now throwing an exception instead of risking executing
locals-cleared garbage.
This article explains how locals clearing enables tail calls
to potentially to inherit strong references to their arguments
from closed over locals.
https://clojure.org/reference/lazy#_dont_hang_onto_your_head
Fixes https://clojure.atlassian.net/browse/CLJ-2861
There are two ways a ^:once function can be called in a delay or lazy-seq:
1. the thread that acquires the lock to call the body then realizes
the same object by calling the body.
2. the body has a (recur) call in tail position.
The first issue can be handled by first checking if the realize lock is already
held by the current thread.
if(l.isHeldByCurrentThread()) {
throw Util.sneakyThrow(Util.runtimeException("Recursive delay dereference"));
}
We do not do this because we don't have access to the locks of Delay and LazySeq.
The second issue could be better supported by the Clojure compiler.
The form (^:once fn [] (recur)) probably should throw a compile-time error.
Instead, we take advantage of locals clearing to detect recursive calls:
(let [x true]
(^:once fn* [] (assert x) (recur)))
This has a runtime cost. An alternative could be move the body out of tail position,
which would result in a compile-time error:
(^:once fn* [] (let [x (recur)] x))
Here we use the assertion approach to handle all cases for portability
and compatibility.
delay
macro
added in 1.0
(delay & body)
Takes a body of expressions and yields a Delay object that will
invoke the body only the first time it is forced (with force or deref/@), and
will cache the result and return it on all subsequent force
calls. See also - realized?
Throws if dereferenced recursively. Explicitly disallows :pre/:post map.
lazy-seq
macro
added in 1.0
(lazy-seq & body)
Takes a body of expressions that returns an ISeq or nil, and yields
a Seqable object that will invoke the body only the first time seq
is called, and will cache the result and return it on all subsequent
seq calls. See also - realized?
Throws if realized recursively. Explicitly disallows :pre/:post map.