Now, how do I write it without using eval?
(defn runonce
"Create a function that will only run once, given a function. Returns
a vector containing the function and the reference that tracks
whether
the function has been run."
[function]
(let [has-run (ref false)]
[(fn [& args]
(or @has-run
; TODO: think through semantics for parallel target invocation
(do
(apply function args)
(dosync (ref-set has-run true)))))
has-run]))
(defmacro defrunonce [sym doc & forms]
"Defines a function with runonce semantics. Curren run status
is kept in a reference under the :has-run metadata key."
(let [[function has-run] (runonce (eval (concat (list 'fn [])
forms)))]
`(def ~(with-meta sym {:has-run has-run}) ~function)))
(defmacro defrunonce [sym doc & forms]
"Defines a function with runonce semantics. Curren run status
is kept in a reference under the :has-run metadata key."
(let [has-run (gensym)]
`(let [[function# ~has-run] (runonce (fn [] ~@forms))]
(def ~(with-meta sym {:has-run has-run}) function#))))
--Chouser
Stuart
>
> The defrunonce macro below works, creating a function that runs only
> once and tracking its run status in metadata.
>
> Now, how do I write it without using eval?
>
> (defn runonce
> "Create a function that will only run once, given a function.
> Returns
> a vector containing the function and the reference that tracks
> whether
> the function has been run."
> [function]
> (let [has-run (ref false)]
> [(fn [& args]
> (or @has-run
> ; TODO: think through semantics for parallel target invocation
> (do
> (apply function args)
> (dosync (ref-set has-run true)))))
> has-run]))
Since the deref of has-run is outside a transaction, I don't see
anything here that prevents two threads from running function if
they're trying to do it at roughly the same time.
I think the following version accomplishes the core task of running
just once and could be adapted to return the has-run ref as well:
(defn runonce
"Create a function that will only run its argument once."
[function]
(let [has-run (ref false)]
(fn [& args]
(when
(dosync
(ensure has-run)
(when-not @has-run
(ref-set has-run true)))
(apply function args)))))
I'd appreciate seeing corrections or simplifications of it.
--Steve
(defn runonce
"Create a function that will only run its argument once."
[function]
(let [call-count (ref 0)]
(fn [& args]
(when (= 1 (dosync (alter call-count inc)))
(apply function args)))))
Or:
(defn runonce
"Create a function that will only run its argument once."
[function]
(let [needed (java.util.concurrent.atomic.AtomicBoolean. true)]
(fn [& args]
(when (.getAndSet needed false)
(apply function args)))))
--Chouser
>
> How about:
>
> (defn runonce
> "Create a function that will only run its argument once."
> [function]
> (let [call-count (ref 0)]
> (fn [& args]
> (when (= 1 (dosync (alter call-count inc)))
> (apply function args)))))
The counter occurred to me too. I don't think it's necessary though.
What's important is that one thread "know" that it was the one that
cause the "false" to "true" transition.
Regarding the value returned by dosync above, I'd believe it if
someone told me that it could only return 1 to one thread, one time,
guaranteed, but I've been wrong before in my reasoning about alter vs.
commute regarding what's possible, so I'll have to give it more
thought on correctness.
Here's perhaps a more basic "one shot" building block:
(defn once
"Returns a function that returns v the first time it's called and
nil every time after that"
[v]
(let [returned (ref false)]
(fn []
(dosync
(when-not (ensure returned)
(ref-set returned true)
v)))))
--Steve
That said, Chouser's AtomicBoolean is probably better unless the run
statuses every need to be composed until a larger transaction.
Just to make things even more fun: *None* of the proposed fixes to the
concurrency bug in the original actually preserve the` semantics of
the original. All have moved from "run (usually) once, mark as done"
to "mark as done, try once". This also means that other threads could
see a runonce as being done when it hasn't begun yet.
Or, long story short, runonce doesn't really work unless you actually
run the function inside the transaction. Fortunately, for my problem
domain I don't need this to be perfect.
Stuart
Am 09.11.2008 um 12:23 schrieb Stuart Halloway:
> Just to make things even more fun: *None* of the proposed fixes to the
> concurrency bug in the original actually preserve the` semantics of
> the original. All have moved from "run (usually) once, mark as done"
> to "mark as done, try once". This also means that other threads could
> see a runonce as being done when it hasn't begun yet.
Hmm.. I have no clue about concurrent programming whatsoever. But
for such cases, maybe an agent would do the trick. Send it the action.
The first time it runs, it calls the function and caches the result.
This could be done maybe with delay/force. Multiple sends would be
automatically be syncronised. But to get the result out of the agent
one would have to pass in a ref, where the results are stored.
(defmacro defrunonce
[n & body]
`(def ~n
(let [agent# (agent (delay ~@body))
retr# (fn [state# r-ref#]
(let [r# (force state#)]
(dosync (ref-set r-ref# r#))))]
(fn []
(let [r-ref# (ref nil)]
(send agent# retr# r-ref#)
(await agent#)
(deref r-ref#))))))
user=> (defrunonce foo (println :Run) 5)
#=(var user/foo)
user=> (foo)
:Run
5
user=> (foo)
5
Ok. Maybe this is also only a hack.
Sincerely
Meikel
Stuart
Wow, that's a comprehensive answer. Thanks.
I am using runonce to create lancet, a build system that ties into
Java's Ant. Build steps definitely do have side effects, so I will
probably end up using the agent approach. Targets don't have (or
ignore) return values, so I could ignore the return entirely. Have to
think about that some more.
Java programmers will suspect that the peek optimization doesn't work
because of its structural similarity to double-checked locking, so
that's going to be fun to explain. :-)
Stuart