"map type maps"
poly-map
provides a highly flexible way to create specialized map-like data structures in Clojure and ClojureScript. It allows you to intercept and customize standard map operations like get
, assoc
, dissoc
, function invocation, printing, and more. This enables built-in validation, side effects, lazy loading, default values, case-insensitive keys, and other custom behaviors without needing to reimplement all the underlying map interfaces.
Think of it as adding middleware or aspects directly to your map data structure. It offers two ways to customize behavior:
:get
, :assoc
) to attach handlers for common map operations. Easier to use for most scenarios in applications or application specific data wrangling.::pm/valAt_k_nf
, ::tpm/assoc_k_v
). Useful for advanced cases or overriding methods not exposed by the high-level API. Prefer the low-level API when building libraries on top of poly-maps. API versions are guaranteed remain stable.Sometimes, you need a map that does more than just associate keys with values. You might want to:
assoc
'd.get
).IFn
) to perform a specific action based on its content.poly-map
provides a structured and composable way to achieve these behaviors by wrapping a standard Clojure(Script) map and delegating operations through customizable handlers.
:get
, :assoc
, :dissoc
, etc.) or low-level method keys.:invoke
high-level keyword or low-level ::pm/invoke
.:print
high-level keyword or low-level keys.::tpm/...
).meta
, with-meta
).Developed and tested with Clojure 1.12.x and ClojureScript 1.12.x.
Add the following dependency:
deps.edn:
com.jolygon/poly-map {:mvn/version "0.1.7"}
Require the main API namespace, aliased as poly
.
(require '[com.jolygon.poly-map.api-0 :as poly :refer [poly-map empty-poly-map]])
Create a poly-map just like a regular map:
(def m1 (poly-map :a 1 :b 2))
;=> {:a 1, :b 2}
;; It behaves like a standard Clojure(Script) map by default:
(get m1 :a) ;=> 1
(get m1 :c 404) ;=> 404
(:b m1) ;=> 2
(count m1) ;=> 2
(assoc m1 :c 3) ;=> {:a 1, :b 2, :c 3} ;; Note: Uses clojure.core/assoc
(dissoc m1 :a) ;=> {:b 2} ;; Note: Uses clojure.core/dissoc
(keys m1) ;=> (:a :b) ;; Order may vary in CLJS
(vals m1) ;=> (1 2) ;; Order may vary in CLJS
;; It's persistent:
(def m2 (assoc m1 :c 3))
m1 ;=> {:a 1, :b 2}
m2 ;=> {:a 1, :b 2, :c 3}
;; Transient support works as expected:
(persistent! (assoc! (transient m1) :d 4))
;=> {:a 1, :b 2, :d 4}
Use poly/assoc
to attach behavior handlers using simple keywords. The first argument is the poly-map, followed by keyword/handler pairs.
(def default-value-map
(-> empty-poly-map
(poly/assoc :get (fn [m k & [nf]]
(get m k (or nf :not-available))))))
(def m-with-default (assoc default-value-map :a 1))
(get m-with-default :a) ;=> 1
(get m-with-default :b) ;=> :not-available
(get m-with-default :b :explicit-nf) ;=> :explicit-nf (uses provided not-found)
(m-with-default :b) ;=> :not-available (:invoke behavior defaults to :get)
;; Example 2: Case-Insensitive String Keys
(defn- normalize-key [k]
(if (string? k) (.toLowerCase ^String k) k))
(def case-insensitive-map
(-> empty-poly-map
(poly/assoc
:assoc (fn [m k v] (assoc m (normalize-key k) v))
:dissoc (fn [m k] (dissoc m (normalize-key k)))
:contains? (fn [m k] (contains? m (normalize-key k)))
:get (fn [m k & [nf]] (get m (normalize-key k) nf)))))
(def headers (-> case-insensitive-map (assoc "Content-Type" "application/json")))
(get headers "content-type") ;=> "application/json"
(contains? headers "CONTENT-TYPE") ;=> true
(dissoc headers "Content-type") ;=> {}
The high-level poly/assoc
function associates handler functions with specific behavior keywords. These keywords generally correspond to common map operations.
:get
: Overrides key lookup (get
, keyword invocation, map-as-function arity-1/arity-2).
(fn [m k] ...)
or (fn [m k nf] ...)
:assoc
: Overrides key/value association (clojure.core/assoc
).
(fn [m k v] ...)
:dissoc
: Overrides key removal (clojure.core/dissoc
).
(fn [m k] ...)
:contains?
: Overrides key presence check (contains?).
(fn [m k] ...)
:invoke
: Overrides map-as-function behavior for all arities.
(fn [m & args] ...)
:print
: Overrides how the map is printed (print-method
, str
).
(fn [m] ...)
(Note: More behaviors might be added. Refer to the com.jolygon.poly-map.api-0/assoc
docstring for the definitive list.)
When you use poly/assoc
, it translates the behavior keyword (e.g., :get
) into one or more low-level implementation keys (e.g., ::pm/valAt_k
, ::pm/valAt_k_nf
) and registers your handler function appropriately using the low-level assoc-impl
mechanism.
For finer control, direct access to underlying protocol/interface methods, or to implement behaviors not covered by the high-level keywords (like complex transient interactions or read-only maps), you can use the low-level API.
Structure: A PolyMap
internally holds:
m
: The underlying persistent map holding the actual data.impls
: A persistent map where keys are specific namespaced keywords and values are functions that override default behavior.metadata
: The map's metadata.Implementation Keys: Override functions are associated with namespaced keyword keys defined in:
com.jolygon.poly-map.api-0.keys
: For persistent map operations (e.g., ::pm/valAt_k_nf
).com.jolygon.poly-map.api-0.trans.keys
: For transient map operations (e.g., ::tpm/assoc_k_v
).Override Function Signatures: Low-level override functions receive more arguments (see keys.cljc
and trans/keys.cljc
docstrings for details). They often need to return a new PolyMap
instance (for persistent ops) or this
(for transient ops).
Providing Implementations: Use poly/assoc-impl
, poly/dissoc-impl
, poly/set-impls
.
;; Example: Read-Only Map (Requires Low-Level API)
(defn read-only-error [& _]
(throw (UnsupportedOperationException. "Map is read-only")))
(def read-only-map-impls
{::pm/assoc_k_v read-only-error ;; Override persistent assoc
::pm/without_k read-only-error ;; Override persistent dissoc
::pm/cons_v read-only-error ;; Override persistent conj
::pm/assocEx_k_v read-only-error
;; Override transient mutations too
::tpm/assoc_k_v read-only-error
::tpm/without_k read-only-error
::tpm/conj_entry read-only-error})
(def read-only-m
(-> (poly-map :a 1)
(poly/set-impls read-only-map-impls))) ;; Use set-impls to replace all impls
;; Usage
(get read-only-m :a) ;=> 1
(try (assoc read-only-m :b 2) (catch Exception e (.getMessage e)))
;=> "Map is read-only"
(try (persistent! (assoc! (transient read-only-m) :c 3)) (catch Exception e (.getMessage e)))
;=> "Map is read-only"
For more detailed examples covering both APIs, see:
poly/assoc
with keywords)poly/assoc-impl
with ::pm/...
keys)Significant performance optimizations have been implemented, including specializing internal types and optimizing constructors.
poly-map
operations (reads, writes, construction, reduction, batch transient updates) now perform very close to, and sometimes exceed, the speed of standard Clojure/Script hash maps and transients.poly-map
at ~95% the speed of standard maps.poly-map
at ~72% the speed of standard maps, heavily influenced by the persistent!
cost. Many individual CLJS operations (writes, reductions) were faster than standard maps.persistent!
), especially in ClojureScript.See ./bench/ex/clj-bench.md for Clojure benchmark details and ./bench/ex/cljs-bench.md for ClojureScript benchmark details. Contributions for further optimization are welcome!
def-map-type
): Potemkin's def-map-type
is excellent for creating new, specific map-like types that efficiently implement map interfaces, often based on delegating to underlying fields or structures. Choose def-map-type
when you need a new, static, record-like data type with map semantics. Choose poly-map
when you want to add dynamic behaviors (validation, logging, computation, interception) to existing map data or general-purpose map structures without defining a whole new type, or when you want to change behaviors dynamically using assoc-impl
/set-impls
.defrecord
/ deftype
: Suitable for creating fixed-schema, efficient data structures. They can implement protocols for map-like behavior, but you implement the methods directly. Less flexible for dynamic behavior modification compared to poly-map
.poly-map
provides a ready-made implementation structure focused specifically on wrapping and intercepting standard map operations.poly-map
handlers (as shown in examples).deftype
or poly-map
's approach.Clone the repository and run tests using the Clojure CLI:
# Clojure tests
clj -X:test-clj
# ClojureScript tests (requires NodeJS)
clj -M:test-cljs
To run benchmarks:
clj -M:libra
Copyright © 2025 Jolygon
Distributed under the MIT license. See LICENSE file for details.
Can you improve this documentation? These fine people already did:
John Newman & John Michael Newman IIIEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close