Liking cljdoc? Tell your friends :D

Concepts

Value Wrapper

A value wrapper is anything wrapped a value inside. The consumer of a wrapper just interests in the value, it is the provider who concerns about how it wraps. In a fun-map, when accessed by key, the consumer just get the wrapped value, ignoring the difference of the wrapper itself. This frees up for consumer to change code if the wrapper itself changes. Practically, the consumer can just assume it is a plain value, fun-map will unwrap it.

Simple value wrappers are clojure.lang.IDeref instances, like delay, future, promise which can not change its wrapped value once realized; atom, ref, agent are also wrappers, but their wrapped value can change. Fun-map blurs the line between all these wrappers. For example:

(def m (fun-map {:numbers (delay [3 4])}))

(defn f [{:keys [numbers]}]
  (apply * numbers))

The author of f can just use plain value map {:a [3 4]} to test, but use m as the argument later.

Function Wrapper

What takes fun-map even further is that a function takes a map as its argument can be treated as value wrapper. The wrapped value will be returned when call it. fw macro will define such wrapper inline:

(def m (fun-map {:numbers (fw {} [3 4])}))

And author of f can also treat it as a normal map!

Chained Function Wrapper

The function wrappers' map argument is the fun-map contained it, so by putting different function wrappers inside a fun-map, meaning it provides a new way to construct a function invoking path.

(def m (fun-map {:numbers [3 4]
                 :cnt (fw {:keys [numbers]} (count numbers))
                 :average (fw {:keys [numbers cnt]}
                            (/ (reduce + 0 numbers) cnt))}))

Accessing :average value of m will make fun-map call it and in turns accessing its :numbers and :cnt values, and the later is another function wrapper, which make the :average function indirectly calling :cnt function inside.

fw macro

fw macro can be used to create an anonymous function with its wrapper like in the above examples.

(fw {:keys [:a/a :b] :or {a 10 b 20}}
  (* a b))

You may notice that fw does not take a vector as its argument, but a map. That’s because a function wrapped in can only take a map as its single argument, so the macro saved you a pair of square bracket. It follows same syntax as standard associative destructrue.

Wrapper decorators

In addition to simple function wrapper, we can use wrapper decorators to provide more, you can use :wrappers key in `fw’s argument map:

(fw {:keys [a] :wrappers []}
  (inc a))

This will create a non-decorated plain function wrapper.

(fw {:keys [a]}
  (inc a))

Without specify :wrappers, the created wrapper will have default decorators [:trace :cache].

:cache decorator

A function wrapper can have a focus function to define whether it should be re-unwrapped, if the return value of the function keeps same, it will just return a cached return value. So the focus function need to be very efficient (at least much faster than the wrapped function itself) and pure functional. If no focus function provided, the function wrapper will just be invoked once.

(def m (fun-map {:numbers (atom [5 3])
                 :other-content (range 1000)
                 :cnt (fw {:keys [numbers] :focus numbers}
                        (count numbers))}))

The function inside :cnt will only be invoked if :numbers changes. fnk macro can be used instead of fw for keys destructuring and focus on these keys:

(fnk [numbers] (count numbers))

:trace decorator

Sometimes you want to know when your function wrapper really called wrapped function, you could attach this trace functions to it by :trace option:

(def m (fun-map {:numbers (atom [5 3])
                 :cnt (fw {:keys [numbers]
                           :trace (fn [k v] (println "key is:" k "value is:" v))}
                        (count numbers))}))

Map shared trace function

The fun-map function itself has a :trace-fn function can apply to all function wrappers inside.

Parallel accessing dependencies

With manifold’s excellent let-flow macro and its future function, if you have it in your class path, specify :par? true in fw macro will make a function wrapper accessing its dependencies in managed threads.

(def m (fun-map {:a (delay (Thread/sleep 3000) 20)
                 :b (delay (Thread/sleep 3000) 30)
                 :c (fw {:keys [a b] :par? true} (* a b))}))

(time (:c m)) ;=> 600 in approx. 3000msec instead of 6000

Gotchas and Common Pitfalls

This section covers behaviors that may surprise new users.

Plain maps inside fun-maps don’t unwrap

Only fun-maps unwrap their values. If you nest a plain map inside a fun-map, values in that plain map are not unwrapped:

;; Plain map nested inside fun-map - delays NOT unwrapped
(def m (fun-map {:a {:b (delay 42)}}))  ; {:b ...} is a plain map
(:b (:a m)) ;=> #object[clojure.lang.Delay ...]

;; Nested fun-map - delays ARE unwrapped
(def m (fun-map {:a (fun-map {:b (delay 42)})}))  ; inner is also a fun-map
(:b (:a m)) ;=> 42
(get-in m [:a :b]) ;=> 42  ; get-in works too

Iteration realizes all values

Iterating a fun-map (keys, vals, seq, reduce-kv, etc.) triggers computation of all values:

(def m (fun-map {:a 1 :b (fnk [a] (println "computing b") (* a 2))}))
(keys m)  ; prints "computing b" even though you only asked for keys!

This is because iteration must produce key-value pairs, which requires unwrapping.

select-keys and into return plain maps

Standard Clojure functions return plain maps, losing fun-map semantics:

(def m (fun-map {:a 1 :b (fnk [a] (* a 2))}))
(type (select-keys m [:a :b])) ;=> clojure.lang.PersistentArrayMap
(type (into {} m))             ;=> clojure.lang.PersistentArrayMap

Values are realized during iteration, so the result contains computed values, not wrappers.

update on computed keys replaces the fnk

Using update on a key with an fnk value realizes the value and replaces the wrapper:

(def m (fun-map {:a 1 :b (fnk [a] (* a 2))}))
(def m2 (update m :b inc))
(:b m2) ;=> 3 (not a function of :a anymore!)

;; The fnk was replaced with the literal value 3
(def m3 (assoc m2 :a 100))
(:b m3) ;=> 3 (unchanged, no longer depends on :a)

dissoc silently breaks dependencies

Removing a key that other `fnk`s depend on doesn’t fail until access:

(def m (fun-map {:a 1 :b (fnk [a] (* a 2))}))
(def m2 (dissoc m :a))
m2 ;=> looks fine: {:b <<unrealized>>}
(:b m2) ;=> Error! :a is missing

:keep-ref + fnk requires careful handling

With :keep-ref true, fnk receives the ref itself, not its value:

(def state (atom [1 2 3]))
(def m (fun-map {:nums state
                 :count (fnk [nums] (count nums))}  ; nums is the atom!
                :keep-ref true))
(:count m) ;=> Error: count not supported on Atom

;; Correct: use fw with explicit deref
(def m (fun-map {:nums state
                 :count (fw {:keys [nums] :focus @nums}
                          (count @nums))}
                :keep-ref true))

Printing shows wrapper state

Printing a fun-map shows raw wrapper state, not computed values:

(def m (fun-map {:a 1 :b (fnk [a] (* a 2))}))
(println m)   ;=> {:a 1, :b <<unrealized>>}
(:b m)        ;=> 2
(println m)   ;=> {:a 1, :b <<2>>}

The <<…​>> notation indicates wrapper values. [unrealized] means not yet computed.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close