wrap
Map ExamplesThis document provides various examples demonstrating how wrap
maps can be used to create specialized map-like structures by overriding default behaviors.
Most examples will require the following namespaces:
(ns ex.examples-low-level-md
(:require
[com.jolygon.wrap-map :as w :refer [wrap empty-wrap vary]]))
Use Case: You want a map that returns a specific default value (or nil
) when a requested key is not found, instead of requiring the caller to provide a nf
argument to get
.
How?: Override :valAt_k
and :invoke-variadic
.
(def default-value-map
(-> empty-wrap
(vary assoc
:valAt_k (fn [_ m k] (get m k :not-available))
:valAt_k_nf (fn [_ m k & [not-available]] (get m k (or not-available :not-available)))
:invoke-variadic (fn [_ m k & [not-available]]
(get m k (or not-available :not-available))))))
(def m1 (assoc default-value-map :a 1))
(get m1 :a) ;=> 1
(m1 :a) ;=> 1 (Arity-1 invoke defaults to get override)
(:a m1) ;=> 1
(get m1 :b) ;=> :not-available
(m1 :b) ;=> :not-available
(:b m1) ;=> :not-available
(get m1 :b :soon) ;=> :soon
(m1 :b :soon) ;=> :soon
(:b m1 :soon) ;=> :soon
Use Case: You need a map where string keys are treated case-insensitively (e.g., for HTTP headers).
How?: Override key lookup and association to normalize string keys (e.g., to lowercase).
(defn- normalize-key [k]
(if (string? k) (.toLowerCase ^String k) k))
(def case-insensitive-map
(-> {}
(vary merge
{:valAt_k (fn [_ m k] (get m (normalize-key k)))
:valAt_k_nf (fn [_ m k nf] (get m (normalize-key k) nf))
:containsKey_k (fn [_ m k] (contains? m (normalize-key k)))
:assoc_k_v (fn [{:as e :keys [<-]} m k v]
(<- e (assoc m (normalize-key k) v)))
:without_k (fn [{:as e :keys [<-]} m k]
(<- e (dissoc m (normalize-key k))))})))
(def headers
(-> case-insensitive-map
(assoc "Content-Type" "application/json")
(assoc :other-header 123)))
(get headers "content-type") ;=> "application/json"
(get headers "CONTENT-TYPE") ;=> "application/json"
(contains? headers "Content-type") ;=> true
(get headers :other-header) ;=> 123 (Non-string keys unaffected)
(dissoc headers "CONTENT-TYPE") ;=> {:other-header 123}
Use Case: Ensure that values associated with specific keys conform to a predefined schema (using spec
in this example).
How?: Override :assoc_k_v
to perform validation before associating.
(require '[clojure.spec.alpha :as s])
(s/def ::name string?)
(s/def ::age pos-int?)
(def schema-map
(-> empty-wrap
(vary assoc
:assoc_k_v (fn [{:as e :keys [<-]} m k v]
(let [expected-type (case k :name ::name :age ::age :any)]
(if (or (= expected-type :any) (s/valid? expected-type v))
(<- e (assoc m k v))
(throw (ex-info "Schema validation failed"
{:key k :value v :expected (s/describe expected-type)}))))))))
(def user (-> schema-map (assoc :name "Alice") (assoc :age 30)))
;=> {:name "Alice", :age 30}
(try
(assoc user :age -5)
(catch Exception e (ex-data e)))
;=> {:key :age, :value -5, :expected pos-int?}
(try
(assoc user :name 123)
(catch Exception e (ex-data e)))
;=> {:key :name, :value 123, :expected string?}
Use Case: Track which keys are being read from the map, perhaps for debugging or analytics.
How?: Override :valAt_k
and :valAt_k_nf
to log the access.
(def access-log (atom []))
(def logging-read-map
(-> {}
(vary assoc
:valAt_k (fn [_ m k]
(swap! access-log conj [:get k])
(get m k))
:valAt_k_nf (fn [_ m k nf]
(swap! access-log conj [:get k nf])
(get m k nf)))))
(def mlog (assoc logging-read-map :a 1))
(reset! access-log [])
(get mlog :a) ;=> 1
(get mlog :b) ;=> nil (Logged as [:get :b])
(get mlog :c 404) ;=> 404
@access-log
;=> [[:get :a] [:get :b] [:get :c 404]]
Use Case: Trigger an external action (like notifying a UI component or saving to a DB) whenever the map is modified.
How?: Override :assoc_k_v
and :without_k
.
(defn notify-change [change-type key value]
(println "[Notification] Type:" change-type ", Key:" key ", Value:" value))
(def notifying-map
(-> {}
(vary assoc
:assoc_k_v (fn [{:as e :keys [<-]} m k v]
(notify-change :assoc k v)
(<- e (assoc m k v)))
:without_k (fn [{:as e :keys [<-]} m k]
(notify-change :dissoc k nil)
(<- e (dissoc m k))))))
(def nmap1 (assoc notifying-map :user "admin"))
; [Notification] Type: :assoc , Key: :user , Value: admin
(def nmap2 (dissoc nmap1 :user))
; [Notification] Type: :dissoc , Key: :user , Value: nil
Use Case: Define keys that don't store a static value but compute one based on other data in the map when accessed.
How?: Override :valAt_k
(and potentially :valAt_k_nf
).
(def computed-prop-map
(-> (wrap :first-name "Jane" :last-name "Doe")
(vary assoc
:valAt_k (fn [_ m k]
(if (= k :full-name)
(str (:first-name m) " " (:last-name m))
(get m k)))
:valAt_k_nf (fn [{:as e :keys [valAt_k]} m k nf]
(if (= k :full-name)
(valAt_k e m k) ;; <- Delegate to valAt_k
(get m k nf))))))
(get computed-prop-map :first-name) ;=> "Jane"
(get computed-prop-map :full-name) ;=> "Jane Doe"
(get computed-prop-map :age :unknown) ;=> :unknown
(get computed-prop-map :full-name :unknown) ;=> "Jane Doe"
Use Case: Defer loading data for certain keys until they are actually requested, perhaps fetching from a database or file.
How?: Override :valAt_k_nf
. If the key isn't present, attempt to load it. This example also updates the map to cache the loaded value.
(defn simulate-db-fetch [k]
(println "[DB] Fetching data for key:" k)
(Thread/sleep 50) ; Simulate delay
(if (= k :user-prefs) {:theme "dark" :lang "en"} nil))
(def lazy-loading-map
(-> {}
(vary assoc
:valAt_k_nf (fn [_ m k nf]
(let [v (get m k ::nf)]
(if (= v ::nf)
(if-let [loaded-val (simulate-db-fetch k)]
(do
(println "[Cache] Storing loaded value for key:" k)
loaded-val) ;; Simple version: just return loaded, no cache update
nf)
v)))
:valAt_k (fn [{:as e :keys [valAt_k_nf]} m k]
(valAt_k_nf e m k ::nf))))) ; Delegate to above
(def lazy-map (assoc lazy-loading-map :config {:port 80}))
(get lazy-map :config) ;=> {:port 80} (No fetch)
(get lazy-map :user-prefs)
; [DB] Fetching data for key: :user-prefs
; [Cache] Storing loaded value for key: :user-prefs
;=> {:theme "dark", :lang "en"}
(get lazy-map :user-prefs) ; Access again
; [DB] Fetching data for key: :user-prefs (Fetched again as simple version doesn't cache)
; [Cache] Storing loaded value for key: :user-prefs
;=> {:theme "dark", :lang "en"}
(get lazy-map :other-key :default)
; [DB] Fetching data for key: :other-key
;=> :default
Use Case: Provide a map interface to data that should not be modified through that interface.
How?: Override all mutating impls (assoc
, dissoc
, conj
, etc.) to throw UnsupportedOperationException
.
(defn read-only-error [& _]
(throw (UnsupportedOperationException. "Wrap map is read-only")))
(def read-only-map-impls
{:assoc_k_v read-only-error
:without_k read-only-error
:assocEx_k_v read-only-error
;; Override transient mutations too if you want `(transient read-only-map)` to fail
:T_assoc_k_v read-only-error
:T_without_k read-only-error
:T_conj_v read-only-error})
(def read-only-m
(-> (wrap :a 1)
(vary merge read-only-map-impls)))
(get read-only-m :a) ;=> 1
(count read-only-m) ;=> 1
(try (assoc read-only-m :b 2) (catch Exception e (.getMessage e)))
;=> "Map is read-only"
(try (dissoc read-only-m :a) (catch Exception e (.getMessage e)))
;=> "Map is read-only"
;; Transient operations also fail if overridden
(try (persistent! (assoc! (transient read-only-m) :c 3)) (catch Exception e (.getMessage e)))
;=> "Map is read-only"
Use Case: Use the map itself as a dispatch mechanism, calling different functions based on arguments passed when the map is invoked.
How?: Override :invoke-variadic
.
(defn handle-add [x y] (+ x y))
(defn handle-multiply [x y] (* x y))
(def dispatching-map
(-> {:add-fn handle-add :mul-fn handle-multiply}
(w/assoc
:invoke-variadic
(fn [_ m operation & args]
(case operation
:add (apply (:add-fn m) args)
:multiply (apply (:mul-fn m) args)
(throw (ex-info "Unknown operation" {:operation operation})))))))
(dispatching-map :add 10 5) ;=> 15
(dispatching-map :multiply 10 5) ;=> 50
(try (dispatching-map :subtract 10 5) (catch Exception e (ex-data e)))
;=> {:operation :subtract}
Use Case: Keep track of how often keys are accessed.
How?: Override :valAt_k
and :valAt_k_nf
. Store counts in an atom external to the map.
(def access-counts (atom {}))
(def counting-map
(-> (wrap :a 1 :b 2)
(w/assoc
:valAt_k
(fn [_ m k]
(swap! access-counts update k (fnil inc 0)) ; Increment count
(get m k))
:valAt_k_nf
(fn [_ m k nf]
(swap! access-counts update k (fnil inc 0)) ; Increment count
(get m k nf)))))
(reset! access-counts {})
(get counting-map :a) ;=> 1
(get counting-map :b) ;=> 2
(get counting-map :a) ;=> 1
(get counting-map :c) ;=> nil
@access-counts
;=> {:a 2, :b 1, :c 1}
Use Case: Perform validation efficiently during batch updates within a transient
/persistent!
block.
How?: Override transient impls like :T_assoc_k_v
.
(def transiently-validating-map
(-> empty-wrap
(vary assoc
:T_assoc_k_v (fn [_ t-m k v]
(if (number? v)
(assoc! t-m k v)
(throw (ex-info "Transient validation failed: Value must be number" {:key k :value v})))))))
;; Successful batch update
(persistent!
(-> transiently-validating-map
transient
(assoc! :x 10)
(assoc! :y 20)))
;=> {:x 10, :y 20}
(try
(persistent!
(-> transiently-validating-map
transient
(assoc! :x 10)
(assoc! :y "not a number"))) ; This will throw
(catch Exception e (ex-data e)))
;=> {:key :y, :value "not a number"}
Use Case: Control how the map is printed or converted to a string, perhaps hiding sensitive data or providing a summary.
How?: Override :print-method_writer
and :toString
.
(def sanitizing-string-map
(-> (wrap :user "secret-user" :id 123 :data [1 2 3])
(w/assoc
:print-method_writer
(fn [_ m w]
(doto w
(.write "<SecureMapData id=")
(.write (str (:id m)))
(.write ">")))
:toString
(fn [_ m]
(str "<SecureMapData id=" (:id m) ">")))))
(str sanitizing-string-map)
;=> "<SecureMapData id=123>"
(println sanitizing-string-map)
; <SecureMapData id=123>
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close