A small Clojure library that implements a two-stage map.
Include the following dependency in your project.clj
file:
:dependencies [[delayed-map "0.1.0-SNAPSHOT"]]
A delayed map is defined by a "seed" map and a loader function that returns another map, the rest of the key-value pairs. This function receives the seed as its sole argument.
(use 'delayed-map.core)
(def lm
(delayed-map {:foo 1 :bar 2}
(fn [seed] {:baz (inc (:bar seed))})))
The loader function will not be called until necessary.
Realization is triggered transparently by one of the following:
- A read on a key that is not part of the seed.
- Any operation that uses the seqable or collection abstractions (
count
,seq
and so on). - Certain map ops like
merge
ordissoc
(see notes below).
user=> lm
{:foo 1, :bar 2, ...}
user=> (:foo lm)
1
user=> (realized? lm)
false
user=> lm
{:foo 1, :bar 2, ...}
user=> (seq lm)
([:bar 2] [:foo 1] [:baz 3])
user=> lm
{:bar 2, :foo 1, :baz 3}
user=> (realized? lm)
true
Operations that won't realize a delayed map include:
assoc
, as it always returns another delayed map with a modified seed for not yet realized delayed maps (it returns a regular map for realized ones).select-keys
, if all the selected keys are present in the seed.
Yes. To maintain Clojure's immutability guarantee on maps, the seed is merged into the result of the loader function, not the other way around.
user=> (seq (delayed-map {:foo "old"} (fn [_] {:foo "new" :bar "hi"})))
([:foo "old"] [:bar "hi"])
I came up with the idea while coding Pundit, a client library for the Parse platform.
The Parse API returns so-called pointers when a value in a field is another database object. A pointer is nothing more than a map with the model class name and the object ID; if you need the full object, you have to load it based on this information.
This process can be made transparent using delayed maps for pointers, delaying the actual request until it becomes necessary.
(defn- load-pointer [{:keys [class-name object-id]}]
(retrieve class-name object-id))
(defn- ptr->obj [ptr]
(delayed-map
(select-keys ptr [:class-name :object-id])
load-pointer))
The decision to prevent print-method
from realizing the map comes also from
this use. Circular references in the database would make a REPL keep on loading
objects forever otherwise (that is, until the heap is blown by the output
buffer).
The dissoc
operation realizes the map even if the key to be removed is in the
seed. The reason for this is that we cannot allow something like the following
to happen:
user=> (def lm (delayed-map {:foo 1 :bar 2} (fn [_] {:bar 3})))
#'user/lm
user=> (:bar (dissoc lm :bar))
3
Copyright © 2014 Roland Venesz / Wopata SARL
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.