On Nov 9, 8:21 am, Stuart Halloway <[EMAIL PROTECTED]> 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

--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Clojure" group.
To post to this group, send email to clojure@googlegroups.com
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/clojure?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to