1

I'm trying to implement a get-database function that retrieves the database reference from Monger the first time it is called, remembers the value in an atom and returns it directly on subsequent calls. My current code looks like this:

(def database (atom nil))

(defn get-database
  []
  (compare-and-set! database nil
    (let [db (:db (mg/connect-via-uri (System/getenv "MONGOLAB_URI")))] db))
  @database)

The problem is that the let clause seems to be evaluated even if compare-and-set! returns false (i.e. database is not nil). Is there some way to get this to evaluate lazily so I don't incur the penalty of retrieving the Monger connection, or is this approach fundamentally misguided?

Matthew Gertner
  • 4,487
  • 2
  • 32
  • 54

1 Answers1

4

The problem here is that compare-and-set! is a function, so evaluating it will evaluate all the parameters before the function is called.

The typical approach that I take for the use case of caching and re-using some expensive-to-compute value is with a delay:

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?

In your case:

(def database (delay (:db (mg/connect-via-uri (System/getenv "MONGOLAB_URI")))))

Now you can just say @database any time you want to get a reference to the database, and the connection will get initialized the first time your code actually causes the delay to be dereferenced. You could wrap the call to dereference the delay inside a get-database function if you'd like, but this isn't necessary.

Alex
  • 13,811
  • 1
  • 37
  • 50
  • Nice. So I guess it's redundant to make `database` an atom in this case? Does the use of a delay guarantee that race conditions won't occur? – Matthew Gertner Oct 13 '14 at 18:41
  • Correct. The delay implementation is thread-safe and guarantees that the body will only be executed once. Attempts to dereference the delay from other threads while the body is still being executed will block until it's completed. – Alex Oct 13 '14 at 19:16
  • Maybe this merits a separate question, but I'm still not entirely clear about concurrent updates of the delay. Is it kosher to change the value of the delay after it has been dereferenced? If so, why does an atom have to be updated using `swap!` (or some other function) but the delay doesn't have any equivalent function? – Matthew Gertner Oct 14 '14 at 09:38
  • A delay has only two states: unrealized (on initial creation) and realized (once forced or dereferenced). Once a delay is realized, its value never changes, which appears to be the semantics you were aiming for with your sample code above. – Alex Oct 14 '14 at 13:00
  • That's right. I was trying to use the same construct for another purpose but based on your comment it wasn't appropriate since I was changing the value after dereferencing. – Matthew Gertner Oct 14 '14 at 15:23