a macro question, this time completely specified :-)

10 views
Skip to first unread message

Stuart Halloway

unread,
Nov 8, 2008, 8:31:19 PM11/8/08
to clo...@googlegroups.com
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]))

(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)))


Chouser

unread,
Nov 8, 2008, 9:13:57 PM11/8/08
to clo...@googlegroups.com
On Sat, Nov 8, 2008 at 8:31 PM, Stuart Halloway
<stuart....@gmail.com> wrote:
>
> (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 Halloway

unread,
Nov 8, 2008, 10:09:05 PM11/8/08
to clo...@googlegroups.com
Excellent, thanks!

Stuart

Stephen C. Gilardi

unread,
Nov 8, 2008, 10:09:57 PM11/8/08
to clo...@googlegroups.com

On Nov 8, 2008, at 8:31 PM, Stuart Halloway wrote:

>
> 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

Stuart Halloway

unread,
Nov 8, 2008, 10:23:30 PM11/8/08
to clo...@googlegroups.com
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)))))

Chouser

unread,
Nov 8, 2008, 10:39:02 PM11/8/08
to clo...@googlegroups.com
On Sat, Nov 8, 2008 at 10:23 PM, Stuart Halloway
<stuart....@gmail.com> wrote:
>
> 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)))))

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

Stephen C. Gilardi

unread,
Nov 8, 2008, 10:42:19 PM11/8/08
to clo...@googlegroups.com

On Nov 8, 2008, at 10:23 PM, Stuart Halloway wrote:

>
> 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

Stuart Halloway

unread,
Nov 9, 2008, 6:23:31 AM11/9/08
to clo...@googlegroups.com
One way to think about the difference between alter and commute: With
a commutative function, they both get to the same end result, but
commute allows more concurrency, while guaranteeing less about the
return value. In particular, with commute you might not be able to see
every step by looking at the return values. If you want to see each
step, as with runonce, you need alter.

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

Meikel Brandmeyer

unread,
Nov 9, 2008, 7:32:28 AM11/9/08
to clo...@googlegroups.com
Hi,

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 Halloway

unread,
Nov 9, 2008, 8:21:54 AM11/9/08
to clo...@googlegroups.com
You should be able to do this without the ref. Have the agent's state
contain a pair of [has-run, fn-result].

Stuart

Rich Hickey

unread,
Nov 9, 2008, 11:02:09 AM11/9/08
to Clojure
On Nov 9, 8:21 am, Stuart Halloway <stuart.hallo...@gmail.com> wrote:
> You should be able to do this without the ref. Have the agent's state
> contain a pair of [has-run, fn-result].

The semantics of your runonce aren't clear to me, but here are some
strategies:

As Chouser proposed, if you only want a one-time effect, atomics are
the simplest. I prefer to use compareAndSet so it's clear what the
governing transition is, and I've written this more verbosely so it
corresponds to the other solutions:

(defn runonce
"Create a function that will only run once. All other invocations
return nil"
[function]
(let [first-call (Object.)
atom (java.util.concurrent.atomic.AtomicReference. first-
call)]
(fn [& args]
(when (.compareAndSet atom first-call nil)
(apply function args)))))

If you don't want any callers to proceed until it has been run once,
then they'll need to queue up somehow. You can use either transactions
or agents, and which to choose depends on whether or not the function
has side-effects. If it doesn't, you can use transactions, the
benefits being multiple such calls can compose:

(defn runonce
"Create a function that will only run once. All other invocations
return the first calculated value. The function must be free of side
effects"
[function]
(let [first-call (Object.)
ret (ref first-call)]
(fn [& args]
(dosync
(when (= @ret first-call)
(ref-set ret (apply function args)))
@ret))))

If the function has side effects, agents are the way to go:

(defn runonce
"Create a function that will only run once. All other invocations
return the first calculated value. The function can have side
effects"
[function]
(let [first-call (Object.)
agt (agent first-call)]
(fn [& args]
(send-off agt
#(if (= % first-call)
(apply function args)
%))
(await agt)
@agt)))

Note that there is no magic bullet here - the agents approach does not
compose, each such function must run autonomously (that's not bad,
just must be understood). This is enforced by the fact that await will
fail if called in an action.

Note also the use of a private sentinel value in order to avoid
managing both the return and the flag.

Both the ref and agent solutions support a 'peek' optimization to
avoid the transaction/action-send:

(defn runonce
"Create a function that will only run once. All other invocations
return the first calculated value. The function must be free of side
effects"
[function]
(let [first-call (Object.)
ret (ref first-call)]
(fn [& args]
(when (= @ret first-call)
(dosync
(when (= @ret first-call)
(ref-set ret (apply function args)))))
@ret)))

(defn runonce
"Create a function that will only run once. All other invocations
return the first calculated value. The function can have side
effects"
[function]
(let [first-call (Object.)
agt (agent first-call)]
(fn [& args]
(when (= @agt first-call)
(send-off agt
#(if (= % first-call)
(apply function args)
%))
(await agt))
@agt)))

You must still re-examine the flag inside the transaction/action.

If you want runonce to return both the once-running function and a way
to detect if it has been run, I recommend returning a has-run fn which
encapsulates the mechanism:

(defn runonce
"Create a function that will only run once. All other invocations
return the first calculated value. The function must be free of side
effects.
Returns [has-run-predicate once-fn]"
[function]
(let [first-call (Object.)
ret (ref first-call)
has-run #(not= @ret first-call)]
[has-run
(fn [& args]
(when-not (has-run)
(dosync
(when-not (has-run)
(ref-set ret (apply function args)))))
@ret)]))

You can do something similar for the agent version.

Rich

Stuart Halloway

unread,
Nov 9, 2008, 2:24:24 PM11/9/08
to clo...@googlegroups.com
Hi Rich,

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

Reply all
Reply to author
Forward
0 new messages