`wrap` maps

Happy Easter Clojure heads! :rabbit: :egg: :candy: :deciduous_tree: :egg: :rabbit: :egg:

What would a holiday be without a new Clojure library? I give to you: wrap maps.

We often leverage Clojure’s powerful immutable maps, but sometimes find ourselves needing slightly different behavior – maybe validating keys/values on assoc, logging specific lookups, providing computed default values, or enforcing other kinds of rules around map interactions. Sprinkling this logic throughout application code isn’t ideal, and modifying the core map types isn’t practical.

What is a wrap map`?

wrap map tackles this by providing a wrapper around existing maps (like hash-map or array-map). It functions just like the map it wraps, but allows you to provide an “implementations environment map” (e) containing functions that override specific map operations.

  • It intercepts calls like get, assoc, dissoc, reduce-kv, invoke (map-as-function), etc.
  • If an override function exists in the e map for that operation, your function is called.
  • If no override exists, the operation delegates to the underlying standard map.
  • It supports both persistent and transient maps (with optimized transient operations).
  • It works in both Clojure (JVM) and ClojureScript environments.

Essentially, it lets you layer behavior on top of maps without changing the underlying data structure, keeping your data pure and your behavioral logic separate.

Who is it For / When to Use It?

This library might be useful if you need to:

  • Add cross-cutting concerns like validation, logging, auditing, or authorization to map operations cleanly.
  • Implement custom map semantics, such as default values for missing keys, simple caching on lookups, or lightweight schema enforcement on writes.
  • Decouple specific behaviors from your core data representation, making components more modular.
  • Do any of the above without modifying standard map implementations or resorting to ad-hoc checks scattered in your code.

When Not to Use It (Performance Considerations)

This flexibility comes with performance trade-offs. While certain paths have been optimized (especially transients), the wrapper introduces indirection. Based on current benchmarks:

  • Good Performance: Basic reads, persistent writes (assoc), into construction, and especially batch transient updates (assoc!) perform very close to standard map speeds in both Clojure and ClojureScript after recent optimizations.
  • Noticeable Overhead: You might see overhead in:
    • Simple assoc calls on small maps on the JVM (though CLJS seems faster here).
    • Any behavior you add the wrap map will likely be much heavier weight than regular map or wrap map operations.
    • apply hash-map style construction, though you can just do (wrap large-map), with no need for application - it just internalizes the map.
    • The persistent! transition from a wrapped transient is slower than native.

Recommendation: If your use case involves performance-critical inner loops that heavily rely on the operations showing overhead, benchmark carefully! wrap map is best suited for situations where the behavioral benefits and code clarity outweigh potential performance costs for specific operations. Don’t use it where a simple helper function or a completely custom data structure (deftype) would be a more direct and performant fit.

Get Involved & Discuss!

This is the initial release! You can find the code, documentation, and more usage examples here:

Try it out and share your thoughts.

  • What interesting use cases or behaviors can you imagine adding with this pattern?
  • Have you encountered similar needs in your projects? How did you solve them?
  • Any feedback on the API or implementation?

Please feel free to open issues on the repository or discuss here.

Thanks!

3 Likes