From 98a90444a912475eb594387071ab2efac1c4195a Mon Sep 17 00:00:00 2001 From: Robert Luo Date: Sun, 18 Jan 2026 15:52:46 +0800 Subject: [PATCH 1/6] bump deps versions --- .github/workflows/main.yml | 10 +++++----- deps.edn | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 83a4259..274ae19 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,22 +14,22 @@ jobs: environment: deploy steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - - uses: DeLaGuardo/setup-clojure@13.4 + - uses: DeLaGuardo/setup-clojure@13.5 with: - cli: '1.12.3.1577' + cli: '1.12.4.1582' - - uses: actions/cache@v4.3.0 + - uses: actions/cache@v5.0.2 with: path: ~/.m2 key: default-build - run: clojure -T:build ci - - uses: codecov/codecov-action@v5.5.1 + - uses: codecov/codecov-action@v5.5.2 - name: deploy if: github.event.release diff --git a/deps.edn b/deps.edn index fa60514..21f5810 100644 --- a/deps.edn +++ b/deps.edn @@ -1,8 +1,8 @@ {:paths ["src"] :aliases {:dev ;for development {:extra-paths ["test"] - :extra-deps {manifold/manifold {:mvn/version "0.4.3"} - org.clojure/clojurescript {:mvn/version "1.11.121"}}} + :extra-deps {manifold/manifold {:mvn/version "0.5.0"} + org.clojure/clojurescript {:mvn/version "1.12.134"}}} :test ;run tests under console. e.g. clj -M:dev:test {:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"} lambdaisland/kaocha-cloverage {:mvn/version "1.1.89"}} @@ -14,4 +14,4 @@ :build ;customized building process running. e.g. clj -T:build ci {:deps {io.github.robertluo/build-clj {:git/sha "5d45f58cc20747c136bb320c9b13d65d2bf4cf58"}} :ns-default build} - :clj-kondo {:extra-deps {clj-kondo/clj-kondo {:mvn/version "2025.09.22"}}}}} + :clj-kondo {:extra-deps {clj-kondo/clj-kondo {:mvn/version "2026.01.12"}}}}} From daccc7059f870f2f55412bc720c222a083779873 Mon Sep 17 00:00:00 2001 From: Robert Luo Date: Sun, 18 Jan 2026 16:31:28 +0800 Subject: [PATCH 2/6] Improve error handling, documentation, and CLJS macro support - Add circular dependency detection with clear error messages showing cycle path - Add better error context for function wrapper failures (key, available keys) - Add comprehensive Gotchas section to doc/concepts.adoc - Make CloseableValue implement java.io.Closeable for with-open support - Remove unnecessary #?(:clj) from defmacro fw/fnk - macros work in CLJS - Rename doc/rational.adoc to doc/rationale.adoc (typo fix) - Add doc/improvements.adoc tracking planned improvements - Update WARP.md with known issues and gotchas - Improve docstrings for fun-map, fw, fnk, closeable, life-cycle-map Co-Authored-By: Warp --- README.adoc | 3 +- WARP.md | 75 ++++++++++ doc/concepts.adoc | 105 ++++++++++++++ doc/improvements.adoc | 174 +++++++++++++++++++++++ doc/{rational.adoc => rationale.adoc} | 0 src/robertluo/fun_map.cljc | 114 +++++++++------ src/robertluo/fun_map/wrapper.cljc | 97 +++++++++++-- test/robertluo/fun_map/wrapper_test.cljc | 40 ++++++ 8 files changed, 553 insertions(+), 55 deletions(-) create mode 100644 WARP.md create mode 100644 doc/improvements.adoc rename doc/{rational.adoc => rationale.adoc} (100%) diff --git a/README.adoc b/README.adoc index 63cc4f1..a6fee9b 100644 --- a/README.adoc +++ b/README.adoc @@ -90,9 +90,10 @@ Some of the most frequently used APIs are: <2> by `assoc` key-value pairs to a fun-map, you turn Datomic API into a map. == Further read - - xref:doc/rational.adoc[Rational] + - xref:doc/rationale.adoc[Rationale] - xref:doc/change_log.adoc[Changes] - xref:doc/concepts.adoc[Concepts] + - xref:doc/improvements.adoc[Planned Improvements] == Development diff --git a/WARP.md b/WARP.md new file mode 100644 index 0000000..1933ea1 --- /dev/null +++ b/WARP.md @@ -0,0 +1,75 @@ +# WARP.md + +This file provides guidance to WARP (warp.dev) when working with code in this repository. + +## Project Overview + +fun-map is a Clojure/ClojureScript library that provides a map data structure where values are automatically unwrapped when accessed. It supports lazy evaluation, dependency injection between map values, and lifecycle management. + +## Build & Test Commands + +```bash +# Run tests in watch mode (auto-reload on changes) +clj -M:dev:test --watch + +# Run all tests once (CLJ + CLJS) +clj -T:build tests + +# Run Clojure tests only +clj -M:dev:test + +# Run ClojureScript tests only +clj -M:dev:cljs-test + +# Build JAR +clj -T:build ci + +# Deploy to Clojars +clj -T:build deploy + +# Copy clj-kondo config to local dev +clj -T:build copy-clj-kondo-config +``` + +## Architecture + +### Core Concepts + +1. **Value Wrappers** (`wrapper.cljc`): Protocol `ValueWrapper` defines how values are unwrapped. Implementations include: + - `FunctionWrapper` - wraps a function that receives the map and key + - `CachedWrapper` - caches results with optional focus function for invalidation + - `TracedWrapper` - adds tracing/logging capability + +2. **DelegatedMap** (`core.cljc`): The main map implementation that: + - Delegates to an underlying map + - Applies `fn-entry` transformation on access to unwrap values + - Implements full Clojure map interfaces for both CLJ and CLJS + +3. **Macros** (`fun_map.cljc`): + - `fw` - Creates function wrappers with full destructuring and options (`:focus`, `:trace`, `:par?`, `:wrappers`) + - `fnk` - Shortcut for `fw` that auto-focuses on declared keys + +### Key Files + +- `src/robertluo/fun_map.cljc` - Public API: `fun-map`, `fw`, `fnk`, `life-cycle-map`, `closeable`, `halt!` +- `src/robertluo/fun_map/core.cljc` - `DelegatedMap` type with CLJ/CLJS implementations +- `src/robertluo/fun_map/wrapper.cljc` - `ValueWrapper` protocol and wrapper types +- `src/robertluo/fun_map/helper.cljc` - Macro helpers for `fw`/`fnk` expansion + +### Testing + +Tests use kaocha with cloverage. Test files mirror source structure in `test/`. + +## Known Issues & Improvements + +See `doc/improvements.adoc` for a tracked list of potential improvements. + +### Key Gotchas + +1. **~~Missing keys produce cryptic errors~~** - FIXED: now throws helpful `ex-info` with context +2. **~~Circular deps cause StackOverflowError~~** - FIXED: now detects cycles and reports the path +3. **Plain maps inside fun-maps don't unwrap** - use nested fun-maps if you need nested unwrapping +4. **Iteration realizes all values** - `keys`, `vals`, `seq` trigger all computations +5. **`select-keys`/`into` return plain maps** - wrappers are lost +6. **`:keep-ref true` + `fnk`** - `fnk` receives atoms, not values; use `fw` with explicit `@` in focus +7. **`update` on computed key** - replaces `fnk` with the updated value diff --git a/doc/concepts.adoc b/doc/concepts.adoc index bc713e0..16a80d9 100644 --- a/doc/concepts.adoc +++ b/doc/concepts.adoc @@ -121,3 +121,108 @@ With manifold's excellent `let-flow` macro and its `future` function, if you hav (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: + +[source,clojure] +---- +;; 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: + +[source,clojure] +---- +(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: + +[source,clojure] +---- +(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: + +[source,clojure] +---- +(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: + +[source,clojure] +---- +(def m (fun-map {:a 1 :b (fnk [a] (* a 2))})) +(def m2 (dissoc m :a)) +m2 ;=> looks fine: {:b <>} +(:b m2) ;=> Error! :a is missing +---- + +### `:keep-ref` + `fnk` requires careful handling + +With `:keep-ref true`, `fnk` receives the ref itself, not its value: + +[source,clojure] +---- +(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: + +[source,clojure] +---- +(def m (fun-map {:a 1 :b (fnk [a] (* a 2))})) +(println m) ;=> {:a 1, :b <>} +(:b m) ;=> 2 +(println m) ;=> {:a 1, :b <<2>>} +---- + +The `<<...>>` notation indicates wrapper values. `<>` means not yet computed. + diff --git a/doc/improvements.adoc b/doc/improvements.adoc new file mode 100644 index 0000000..2d240fa --- /dev/null +++ b/doc/improvements.adoc @@ -0,0 +1,174 @@ += Improvement Ideas +:toc: + +This document records potential improvements identified through REPL exploration and API review. + +== 1. Better Error Messages + +*Priority:* High + +*Problem:* +Missing keys produce cryptic errors: +[source] +---- +NullPointerException: Cannot invoke "Object.getClass()" because "x" is null +---- + +Circular dependencies cause `StackOverflowError` with no indication of the cycle. + +*Examples:* +[source,clojure] +---- +(def m (fun-map {:a (fnk [missing-key] (inc missing-key))})) +(:a m) ;=> NullPointerException + +(def m (fun-map {:a (fnk [b] b) :b (fnk [a] a)})) +(:a m) ;=> StackOverflowError +---- + +*Suggestion:* + +- Detect missing dependencies and throw `ex-info` with `:missing-key` and `:dependent-key` +- Track access stack to detect and report circular dependencies with the cycle path + +== 2. Plain Maps Inside Fun-Maps Don't Unwrap + +*Priority:* Low (documentation) + +*Problem:* +Only fun-maps unwrap their values. Plain maps nested inside a fun-map do not unwrap: + +[source,clojure] +---- +(def m (fun-map {:a {:b (delay 42)}})) ; {:b ...} is a plain map +(:b (:a m)) ;=> #object[clojure.lang.Delay ...] ; not 42 + +;; With nested fun-map, it works: +(def m (fun-map {:a (fun-map {:b (delay 42)})})) +(get-in m [:a :b]) ;=> 42 +---- + +*Status:* Documented in concepts.adoc. This is expected behavior, not a bug. + +== 3. Iteration Triggers All Computations + +*Priority:* Low (documentation) + +*Problem:* +Iterating a fun-map (including `keys`, `vals`, `seq`, `reduce-kv`) triggers all computations. Side effects interleave with output: + +[source,clojure] +---- +(def m (fun-map {:a 1 :b (fnk [a] (println "computing") (* a 2))})) +(keys m) +;; prints: (:a computing +;; :b) +---- + +*Suggestion:* +Document that iteration realizes all values. This is expected but may surprise users. + +== 4. `select-keys` and `into` Lose Fun-Map Semantics + +*Priority:* Low + +*Problem:* +Standard Clojure functions return plain maps: + +[source,clojure] +---- +(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 +---- + +*Suggestion:* +Document as expected behavior (values are realized during iteration). Optionally provide `select-keys*` that preserves wrappers for advanced use cases. + +== 5. `:keep-ref` Interaction with `fnk` Is Confusing + +*Priority:* Medium + +*Problem:* +With `:keep-ref true`, `fnk` receives the atom itself, not the dereferenced value. But `fnk` auto-generates focus on the *binding* (the atom), not its contents. + +[source,clojure] +---- +(def state (atom [1 2 3])) +(def m (fun-map {:nums state + :count (fnk [nums] (count nums))} ; nums is atom, not vector! + :keep-ref true)) +(:count m) ;=> UnsupportedOperationException: count not supported on Atom +---- + +The correct pattern requires explicit `fw` with `@` in focus: +[source,clojure] +---- +(fw {:keys [nums] :focus @nums} + (count @nums)) +---- + +*Suggestion:* + +- Document this interaction explicitly in the API docs +- Consider a `:deref-focus` option for `fnk` that focuses on `@binding` instead of `binding` + +== 6. Documentation: Missing "Gotchas" Section + +*Priority:* High + +*Problem:* +Several behaviors are surprising to new users: + +1. `update` on a computed key replaces the `fnk` with the computed+updated value +2. Printing a fun-map with unrealized values shows `<>` +3. `dissoc` a dependency breaks dependent `fnk`s silently until accessed + +*Examples:* +[source,clojure] +---- +;; update replaces fnk +(def m (fun-map {:a 1 :b (fnk [a] (* a 2))})) +(def m2 (update m :b inc)) +(:b m2) ;=> 3 ; fnk is gone, now just the value 3 + +;; dissoc breaks silently +(def m (fun-map {:a 1 :b (fnk [a] (* a 2))})) +(def m2 (dissoc m :a)) +(:b m2) ;=> NullPointerException (only when accessed) +---- + +*Suggestion:* +Add a "Gotchas" or "Common Pitfalls" section to README or concepts.adoc. + +== 7. `closeable` Could Support `with-open` + +*Priority:* Low + +*Problem:* +`CloseableValue` implements `Haltable` but not `java.io.Closeable`, so it doesn't work with Clojure's `with-open`. + +[source,clojure] +---- +(with-open [r (closeable (create-resource) #(cleanup))] + (use-resource @r)) +;; Error: closeable doesn't implement Closeable +---- + +*Suggestion:* +Extend `CloseableValue` to implement `java.io.Closeable` in CLJ for better interop. The `close` method would delegate to `halt!`. + +== Implementation Status + +[cols="1,3,1"] +|=== +|ID |Description |Status + +|1 |Better error messages |DONE +|2 |Document `get-in` limitation |DONE +|3 |Document iteration behavior |DONE +|4 |Document `select-keys`/`into` behavior |DONE +|5 |Document `:keep-ref` + `fnk` interaction |DONE +|6 |Add gotchas section |DONE +|7 |`Closeable` support for `closeable` |DONE +|=== diff --git a/doc/rational.adoc b/doc/rationale.adoc similarity index 100% rename from doc/rational.adoc rename to doc/rationale.adoc diff --git a/src/robertluo/fun_map.cljc b/src/robertluo/fun_map.cljc index 2d9fbf1..6005cd7 100644 --- a/src/robertluo/fun_map.cljc +++ b/src/robertluo/fun_map.cljc @@ -12,9 +12,9 @@ A fun-map is a special map which will automatically *unwrap* a value if it's a wrapper when accessed by key. A wrapper is anything which wrapped a ordinary value inside. Many clojure data structures are wrapper, such as atom, ref, future, delay, - agent etc. In fact, anything implements clojure.lang.IDRef interface is a wrapper. + agent etc. In fact, anything implements clojure.lang.IDeref interface is a wrapper. - FuntionWrapper is another wrapper can be used in a fun-map, which wraps a function, + FunctionWrapper is another wrapper can be used in a fun-map, which wraps a function, it will be called with the fun-map itself as the argument. Map m is the underlying storage of a fun-map, fun-map does not change its property @@ -22,12 +22,17 @@ Options: - - ::trace-fn An Effectful function for globally FunctionWrapper calling trace which - accept key and value as its argument. + - :trace-fn An effectful function for globally tracing FunctionWrapper calls. + Accepts key and value as arguments. + - :keep-ref When true, IDeref values (delay, future, atom, etc.) will NOT be + automatically dereferenced. Use this when you want to store refs + as actual values. Individual values can still use `fnk` or `fw` + to opt into lazy evaluation. Example: - (fun-map {:a 35 :b (delay (println \"hello from b!\"))}" + (fun-map {:a 35 :b (delay (+ 5 3))}) ; (:b m) => 8 + (fun-map {:a (atom 1)} :keep-ref true) ; (:a m) => #" [m & {:keys [trace-fn keep-ref]}] (with-meta (core/delegate-map m wrapper/wrapper-entry) @@ -42,25 +47,30 @@ (fun-map {:a 1 :b 5 :c (wrapper/fun-wrapper (fn [m _] (let [a (get m :a) b (get m :b)] (+ a b))))}) ) -#?(:clj - (defmacro fw - "Returns a FunctionWrapper of an anonymous function defined by body. +(defmacro fw + "Returns a FunctionWrapper of an anonymous function defined by body. Since a FunctionWrapper's function will be called with the map itself as the - argument, this macro using a map `arg-map` as its argument. It follows the - same syntax of clojure's associative destructure. You may use `:keys`, `:as`, + argument, this macro uses a map `arg-map` as its argument. It follows the + same syntax of Clojure's associative destructuring. You may use `:keys`, `:as`, `:or` inside. - Special key `:wrappers` specify additional wrappers of function wrapper: + Options (special keys in arg-map): + + - `:wrappers` Controls caching and tracing behavior: + - `[]` No caching, no tracing. Function called on every access. + - (default) `[:trace :cache]` - cached and traceable (see below). + + - `:focus` A form evaluated to determine if cached value is stale. + Must be pure and efficient. If the focus value changes, + the function is re-evaluated. Without `:focus`, the function + is called only once (memoized). - - `[]` for naive one, no cache, no trace. - - default to specable cached traceable implementation. which supports special keys: - - `:focus` A form that will be called to check if the function itself need - to be called. It must be pure functional and very effecient. - - `:trace` A trace function, if the value updated, it will be called with key - and the function's return value. + - `:trace` A function `(fn [k v] ...)` called when the wrapped function + is actually invoked (not on cache hits). - Special option `:par? true` will make dependencies accessing parallel. + - `:par?` When true, dependencies are accessed in parallel using + manifold's `let-flow`. Requires manifold on classpath. Example: @@ -68,10 +78,12 @@ :as m :trace (fn [k v] (println k v)) :focus (select-keys m [:a :b])} - (+ a b))" - {:style/indent 1} - [arg-map & body] - (helper/make-fw-wrapper `wrapper/fun-wrapper [:trace :cache] arg-map body))) + (+ a b)) + + Works in both Clojure and ClojureScript." + {:style/indent 1} + [arg-map & body] + (helper/make-fw-wrapper `wrapper/fun-wrapper [:trace :cache] arg-map body)) #?(:clj (defmethod helper/fw-impl :trace @@ -85,16 +97,25 @@ `(fn [~arg-map] ~focus))] `(wrapper/cache-wrapper ~f ~focus)))) -#?(:clj - (defmacro fnk - "A shortcut for `fw` macro. Returns a simple FunctionWrapper which depends on - `args` key of the fun-map, it will *focus* on the keys also." - {:style/indent 1} - [args & body] - (let [focus (mapv (comp symbol name) args)] - `(fw {:keys ~args - :focus ~focus} - ~@body)))) +(defmacro fnk + "A shortcut for `fw` macro. Returns a cached FunctionWrapper that: + 1. Destructures the specified keys from the fun-map + 2. Automatically focuses on those keys (re-evaluates when they change) + + Equivalent to: + (fnk [a b] body) => (fw {:keys [a b] :focus [a b]} body) + + Note: Namespace qualifiers on keys are used for destructuring but stripped + for focus comparison. E.g., `(fnk [:ns/a] ...)` destructures `:ns/a` but + focuses on the local binding `a`. + + Works in both Clojure and ClojureScript." + {:style/indent 1} + [args & body] + (let [focus (mapv (comp symbol name) args)] + `(fw {:keys ~args + :focus ~focus} + ~@body))) (comment (macroexpand-1 '(fnk [a :ns/b] (+ a b))) @@ -126,13 +147,13 @@ (close-fn this))))) (defn life-cycle-map - "returns a fun-map can be shutdown orderly. + "Returns a fun-map that can be shutdown orderly. - Any FunctionWrapper supports `Closeable` in this map will be considered - as a component, its `close` method will be called in reversed order of its - creation when the map itself closing. + Any value satisfying the `Haltable` protocol in this map will be considered + a component. Its `halt!` method will be called in reverse order of creation + when the map itself is halted via `(halt! the-map)`. - Notice only accessed components will be shutdown." + Note: Only accessed components will be shutdown." [m] (let [components (atom []) trace-fn (fn [_ v] @@ -152,14 +173,23 @@ :cljs (-deref [_] value)) Haltable (halt! [_] - (close-fn))) + (close-fn)) + #?@(:clj + [java.io.Closeable + (close [this] + (halt! this))])) (defn closeable - "Returns a wrapped plain value, which implements IDref and Closeable, - the close-fn is an effectual function with no argument. + "Returns a wrapped plain value which implements IDeref, Haltable, and (in CLJ) + java.io.Closeable. The close-fn is an effectful function with no arguments. + + When used inside a life-cycle-map, close-fn will be called when + halting the map via `(halt! the-map)`. + + In Clojure, the returned value works with `with-open`: - When used inside a life cycle map, its close-fn when get called when - closing the map." + (with-open [conn (closeable (create-conn) #(close-conn conn))] + (use-conn @conn))" [r close-fn] (->CloseableValue r close-fn)) diff --git a/src/robertluo/fun_map/wrapper.cljc b/src/robertluo/fun_map/wrapper.cljc index d413440..93f59ef 100644 --- a/src/robertluo/fun_map/wrapper.cljc +++ b/src/robertluo/fun_map/wrapper.cljc @@ -1,5 +1,6 @@ (ns robertluo.fun-map.wrapper - "Protocols that sharing with other namespaces") + "Protocols that sharing with other namespaces" + (:require [clojure.string :as str])) (defprotocol ValueWrapper "A wrapper for a value." @@ -8,6 +9,11 @@ (-unwrap [this m k] "unwrap the real value from a wrapper on the key of k")) +;; Track keys currently being unwrapped to detect circular dependencies +(def ^:dynamic *unwrap-stack* + "Stack of keys currently being unwrapped. Used for cycle detection." + []) + ;; Make sure common value is not wrapped #?(:clj (extend-protocol ValueWrapper @@ -25,11 +31,57 @@ (-unwrap [d _ _] (deref d)))) +(defn- raw-keys + "Get raw keys from a map without triggering unwrapping. + In CLJ, uses rawSeq to avoid triggering unwrapping. + In CLJS, falls back to regular keys (only used in error messages)." + [m] + #?(:clj + (if (instance? robertluo.fun_map.core.IFunMap m) + (map first (.rawSeq ^robertluo.fun_map.core.IFunMap m)) + (keys m)) + :cljs + ;; In CLJS we can't easily reference core without circular dep, + ;; and this is only used for error context, so just use keys on underlying map + (if-let [raw-seq-fn (some-> m meta ::-raw-seq-fn)] + (map first (raw-seq-fn)) + (keys m)))) + +(defn- wrap-fn-error + "Wraps exceptions from function wrapper with context about key and access path." + [f m k] + (try + (f m k) + (catch #?(:clj NullPointerException :cljs js/Error) e + (throw (ex-info (str "Error computing key " (pr-str k) + (when (seq *unwrap-stack*) + (str " (dependency chain: " + (str/join " -> " (map pr-str *unwrap-stack*)) + ")")) + ": " #?(:clj (.getMessage e) :cljs (.-message e)) + ". Possible cause: a dependency key is missing from the map.") + {:type :function-wrapper-error + :key k + :access-path *unwrap-stack* + :available-keys (set (raw-keys m))} + e))) + (catch #?(:clj Exception :cljs :default) e + (throw (ex-info (str "Error computing key " (pr-str k) + (when (seq *unwrap-stack*) + (str " (dependency chain: " + (str/join " -> " (map pr-str *unwrap-stack*)) + ")")) + ": " #?(:clj (.getMessage e) :cljs (.-message e))) + {:type :function-wrapper-error + :key k + :access-path *unwrap-stack*} + e))))) + (deftype FunctionWrapper [f] ValueWrapper (-wrapped? [_ _] true) (-unwrap [_ m k] - (f m k)) + (wrap-fn-error f m k)) #?@(:cljs [IPrintWithWriter (-pr-writer @@ -41,19 +93,40 @@ "construct a new FunctionWrapper" ->FunctionWrapper) +(defn- check-cycle! + "Throws if k is already in the unwrap stack (circular dependency)." + [k] + (when (some #{k} *unwrap-stack*) + (let [cycle-path (conj *unwrap-stack* k)] + (throw (ex-info (str "Circular dependency detected: " + (str/join " -> " (map pr-str cycle-path))) + {:type :circular-dependency + :key k + :cycle cycle-path}))))) + +(defn- unwrap-with-tracking + "Unwrap a value with cycle tracking. Returns the unwrapped value." + [v m k] + (check-cycle! k) + (binding [*unwrap-stack* (conj *unwrap-stack* k)] + (-unwrap v m k))) + (defn wrapper-entry "returns a k,v pair from map `m` and input k-v pair. - If `v` is a wrapped, then recursive unwrap it." + If `v` is a wrapped, then recursive unwrap it. + Detects circular dependencies and provides helpful error messages." [m [k v]] - #?(:clj - (if (-wrapped? v m) - (recur m [k (-unwrap v m k)]) - [k v]) - :cljs - (cond - (satisfies? ValueWrapper v) (recur m [k (-unwrap v m k)]) - (satisfies? IDeref v) (recur m [k (deref v)]) - :else [k v]))) + (loop [v v] + #?(:clj + (if (-wrapped? v m) + (recur (unwrap-with-tracking v m k)) + [k v]) + :cljs + (cond + (satisfies? ValueWrapper v) + (recur (unwrap-with-tracking v m k)) + (satisfies? IDeref v) (recur (deref v)) + :else [k v])))) ;;;;;;;;;;; High order wrappers diff --git a/test/robertluo/fun_map/wrapper_test.cljc b/test/robertluo/fun_map/wrapper_test.cljc index 0a07028..40f99d7 100644 --- a/test/robertluo/fun_map/wrapper_test.cljc +++ b/test/robertluo/fun_map/wrapper_test.cljc @@ -1,6 +1,7 @@ (ns robertluo.fun-map.wrapper-test (:require [clojure.test :refer [deftest is testing]] + [robertluo.fun-map :refer [fun-map fnk]] [robertluo.fun-map.wrapper :as sut])) (deftest wrapper-entry @@ -8,3 +9,42 @@ (is (= [:b 1] (sut/wrapper-entry {:a 1} [:b (sut/fun-wrapper (fn [m _] (get m :a)))])))) (testing "wrapped entry will unwrap deeply until geeting non-wrapped value" (is (= [:b 6] (sut/wrapper-entry {:a 3} [:b (atom (atom 6))]))))) + +(deftest circular-dependency-detection + (testing "self-referencing key throws with cycle info" + (let [m (fun-map {:a (fnk [a] a)})] + (is (thrown-with-msg? #?(:clj clojure.lang.ExceptionInfo :cljs ExceptionInfo) + #"Circular dependency detected: :a -> :a" + (:a m))))) + (testing "two-key cycle throws with cycle path" + (let [m (fun-map {:a (fnk [b] b) :b (fnk [a] a)})] + (is (thrown-with-msg? #?(:clj clojure.lang.ExceptionInfo :cljs ExceptionInfo) + #"Circular dependency detected: :a -> :b -> :a" + (:a m))))) + (testing "three-key cycle throws with full path" + (let [m (fun-map {:a (fnk [b] b) :b (fnk [c] c) :c (fnk [a] a)})] + (is (thrown-with-msg? #?(:clj clojure.lang.ExceptionInfo :cljs ExceptionInfo) + #"Circular dependency detected: :a -> :b -> :c -> :a" + (:a m))))) + (testing "ex-data contains cycle information" + (let [m (fun-map {:a (fnk [b] b) :b (fnk [a] a)}) + ex (try (:a m) nil (catch #?(:clj Exception :cljs :default) e e))] + (is (= :circular-dependency (:type (ex-data ex)))) + (is (= :a (:key (ex-data ex)))) + (is (= [:a :b :a] (:cycle (ex-data ex))))))) + +;; Note: These tests are CLJ-only because in CLJS, (inc nil) returns NaN +;; instead of throwing an exception due to JavaScript's type coercion. +#?(:clj + (deftest error-context-on-failure + (testing "NPE in fnk body provides context about key" + (let [m (fun-map {:a (fnk [missing] (inc missing))}) + ex (try (:a m) nil (catch Exception e e))] + (is (= :function-wrapper-error (:type (ex-data ex)))) + (is (= :a (:key (ex-data ex)))) + (is (= #{:a} (:available-keys (ex-data ex)))))) + (testing "error message mentions the key being computed" + (let [m (fun-map {:result (fnk [x] (inc x))})] + (is (thrown-with-msg? clojure.lang.ExceptionInfo + #"Error computing key :result" + (:result m))))))) From 759885e291308812495ef0510b1529412722384a Mon Sep 17 00:00:00 2001 From: Robert Luo Date: Sun, 18 Jan 2026 21:37:43 +0800 Subject: [PATCH 3/6] CI: Add Java 21 setup for ClojureScript compatibility Google Closure Compiler in CLJS 1.12.x requires Java 21. Co-Authored-By: Warp --- .github/workflows/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 274ae19..0ae4f8e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,6 +18,11 @@ jobs: with: fetch-depth: 0 + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + - uses: DeLaGuardo/setup-clojure@13.5 with: cli: '1.12.4.1582' From daa3022b101d2d9b33d93233c3fdf0be84d07905 Mon Sep 17 00:00:00 2001 From: Robert Luo Date: Sun, 18 Jan 2026 21:40:51 +0800 Subject: [PATCH 4/6] Update improvements.adoc to reflect completed work Co-Authored-By: Warp --- doc/improvements.adoc | 81 +++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 53 deletions(-) diff --git a/doc/improvements.adoc b/doc/improvements.adoc index 2d240fa..6edb48c 100644 --- a/doc/improvements.adoc +++ b/doc/improvements.adoc @@ -1,36 +1,30 @@ = Improvement Ideas :toc: -This document records potential improvements identified through REPL exploration and API review. +This document records improvements identified through REPL exploration and API review. -== 1. Better Error Messages +== 1. Better Error Messages ✓ -*Priority:* High +*Status:* DONE -*Problem:* -Missing keys produce cryptic errors: -[source] ----- -NullPointerException: Cannot invoke "Object.getClass()" because "x" is null ----- +*Original problem:* +Missing keys produced cryptic `NullPointerException`. Circular dependencies caused `StackOverflowError` with no indication of the cycle. -Circular dependencies cause `StackOverflowError` with no indication of the cycle. +*Solution implemented:* -*Examples:* +- Added `*unwrap-stack*` dynamic var to track keys being unwrapped +- Circular dependencies now throw `ex-info` with `:type :circular-dependency` and full cycle path +- Function wrapper errors now include context: key being computed, available keys, dependency chain + +*Example of new behavior:* [source,clojure] ---- -(def m (fun-map {:a (fnk [missing-key] (inc missing-key))})) -(:a m) ;=> NullPointerException - (def m (fun-map {:a (fnk [b] b) :b (fnk [a] a)})) -(:a m) ;=> StackOverflowError +(:a m) +;=> ExceptionInfo: Circular dependency detected: :a -> :b -> :a +; {:type :circular-dependency, :key :a, :cycle [:a :b :a]} ---- -*Suggestion:* - -- Detect missing dependencies and throw `ex-info` with `:missing-key` and `:dependent-key` -- Track access stack to detect and report circular dependencies with the cycle path - == 2. Plain Maps Inside Fun-Maps Don't Unwrap *Priority:* Low (documentation) @@ -113,51 +107,32 @@ The correct pattern requires explicit `fw` with `@` in focus: - Document this interaction explicitly in the API docs - Consider a `:deref-focus` option for `fnk` that focuses on `@binding` instead of `binding` -== 6. Documentation: Missing "Gotchas" Section - -*Priority:* High - -*Problem:* -Several behaviors are surprising to new users: - -1. `update` on a computed key replaces the `fnk` with the computed+updated value -2. Printing a fun-map with unrealized values shows `<>` -3. `dissoc` a dependency breaks dependent `fnk`s silently until accessed +== 6. Documentation: Missing "Gotchas" Section ✓ -*Examples:* -[source,clojure] ----- -;; update replaces fnk -(def m (fun-map {:a 1 :b (fnk [a] (* a 2))})) -(def m2 (update m :b inc)) -(:b m2) ;=> 3 ; fnk is gone, now just the value 3 +*Status:* DONE -;; dissoc breaks silently -(def m (fun-map {:a 1 :b (fnk [a] (* a 2))})) -(def m2 (dissoc m :a)) -(:b m2) ;=> NullPointerException (only when accessed) ----- +*Solution:* Added comprehensive "Gotchas and Common Pitfalls" section to `doc/concepts.adoc` covering: -*Suggestion:* -Add a "Gotchas" or "Common Pitfalls" section to README or concepts.adoc. +- Plain maps inside fun-maps don't unwrap +- Iteration realizes all values +- `select-keys` and `into` return plain maps +- `update` on computed keys replaces the fnk +- `dissoc` silently breaks dependencies +- `:keep-ref` + `fnk` interaction +- Printing shows wrapper state -== 7. `closeable` Could Support `with-open` +== 7. `closeable` Could Support `with-open` ✓ -*Priority:* Low +*Status:* DONE -*Problem:* -`CloseableValue` implements `Haltable` but not `java.io.Closeable`, so it doesn't work with Clojure's `with-open`. +*Solution:* `CloseableValue` now implements `java.io.Closeable` in CLJ. Works with `with-open`: [source,clojure] ---- (with-open [r (closeable (create-resource) #(cleanup))] - (use-resource @r)) -;; Error: closeable doesn't implement Closeable + (use-resource @r)) ; works! ---- -*Suggestion:* -Extend `CloseableValue` to implement `java.io.Closeable` in CLJ for better interop. The `close` method would delegate to `halt!`. - == Implementation Status [cols="1,3,1"] From 088ffe18f03655b6c9763a541d8fc35ef48bced9 Mon Sep 17 00:00:00 2001 From: Robert Luo Date: Mon, 19 Jan 2026 08:11:54 +0800 Subject: [PATCH 5/6] Improve core.cljc: fix nil/false handling, add IKVReduce, document gotchas - Fix valAt/lookup nil/false handling in both CLJ and CLJS Previously (get m :key :not-found) returned :not-found when value was nil/false - Add IKVReduce implementation for better reduce-kv performance - Add ILookup to CLJS TransientDelegatedMap - Document hash/equals gotchas in concepts.adoc - Add tests for nil/false value handling - Fix typo: iterface -> interface Co-Authored-By: Warp --- doc/concepts.adoc | 26 +++++++++++++ doc/improvements.adoc | 28 ++++++++++++++ src/robertluo/fun_map/core.cljc | 54 +++++++++++++++++++-------- test/robertluo/fun_map/core_test.cljc | 12 ++++++ 4 files changed, 104 insertions(+), 16 deletions(-) diff --git a/doc/concepts.adoc b/doc/concepts.adoc index 16a80d9..ab6144d 100644 --- a/doc/concepts.adoc +++ b/doc/concepts.adoc @@ -226,3 +226,29 @@ Printing a fun-map shows raw wrapper state, not computed values: The `<<...>>` notation indicates wrapper values. `<>` means not yet computed. +### Equality comparison realizes all values + +Comparing a fun-map with `=` triggers computation of all values: + +[source,clojure] +---- +(def m (fun-map {:a 1 :b (fnk [a] (println "computing") (* a 2))})) +(= m {:a 1 :b 2}) ; prints "computing" +---- + +This is necessary for correct equality semantics. + +### Hash codes use underlying map, not realized values + +`hashCode` and `hasheq` are computed from the underlying map (with wrappers), not the realized values. This means two fun-maps that are `=` may have different hash codes if their wrappers differ: + +[source,clojure] +---- +(def m1 (fun-map {:a (fnk [] 1)})) +(def m2 (fun-map {:a (delay 1)})) +(= m1 m2) ;=> true +(= (hash m1) (hash m2)) ;=> likely false! +---- + +Avoid using fun-maps as hash map keys or in sets if wrapper identity matters. + diff --git a/doc/improvements.adoc b/doc/improvements.adoc index 6edb48c..1686d59 100644 --- a/doc/improvements.adoc +++ b/doc/improvements.adoc @@ -133,6 +133,33 @@ The correct pattern requires explicit `fw` with `@` in focus: (use-resource @r)) ; works! ---- +== 8. core.cljc Improvements ✓ + +*Status:* DONE + +Several issues were identified and fixed in `core.cljc`: + +=== Fixed: `valAt` nil/false handling (High priority) + +Both CLJ and CLJS `valAt`/`-lookup` with `not-found` incorrectly returned `not-found` when the computed value was `nil` or `false`. + +*Solution:* Changed from `(or ... not-found)` to `(if-let [entry ...] (val entry) not-found)`. + +=== Added: `IKVReduce` implementation (Low priority) + +`reduce-kv` now uses native implementation instead of falling back to seq-based reduction. + +=== Added: CLJS `TransientDelegatedMap` `ILookup` (Low priority) + +CLJS transient fun-maps now support lookups during transient operations. + +=== Documented: Hash/equals gotchas + +- Equality comparison realizes all values +- Hash codes use underlying map (wrappers), not realized values + +See `doc/concepts.adoc` for details. + == Implementation Status [cols="1,3,1"] @@ -146,4 +173,5 @@ The correct pattern requires explicit `fw` with `@` in focus: |5 |Document `:keep-ref` + `fnk` interaction |DONE |6 |Add gotchas section |DONE |7 |`Closeable` support for `closeable` |DONE +|8 |core.cljc improvements |DONE |=== diff --git a/src/robertluo/fun_map/core.cljc b/src/robertluo/fun_map/core.cljc index 3df1cd1..dbf3757 100644 --- a/src/robertluo/fun_map/core.cljc +++ b/src/robertluo/fun_map/core.cljc @@ -7,7 +7,7 @@ ATransientMap]))) #?(:clj -;;Marker iterface for a funmap +;; Marker interface for a funmap (definterface IFunMap (rawSeq [])) :cljs @@ -59,7 +59,18 @@ (->DelegatedMap (-persistent! tm) fn-entry)) (-conj! [_ pair] - (TransientDelegatedMap. (-conj! tm pair) fn-entry)))) + (TransientDelegatedMap. (-conj! tm pair) fn-entry)) + + ILookup + (-lookup + [this k] + (-lookup this k nil)) + (-lookup + [this k not-found] + (if-let [entry (when (-contains-key? tm k) + (fn-entry this (-find tm k)))] + (val entry) + not-found)))) #?(:clj ;; DelegatedMap takes a map `m` and delegates most feature to it. @@ -90,13 +101,13 @@ clojure.lang.IFn (invoke [this k] (.valAt this k)) (invoke [this k not-found] (.valAt this k not-found)) - clojure.lang.ILookup - (valAt [this k] - (some-> ^IMapEntry (.entryAt this k) (.val))) - (valAt [this k not-found] - (if (.containsKey this k) - (.valAt this k) - not-found)) + clojure.lang.ILookup + (valAt [this k] + (some-> ^IMapEntry (.entryAt this k) (.val))) + (valAt [this k not-found] + (if-let [entry (.entryAt this k)] + (.val ^IMapEntry entry) + not-found)) clojure.lang.IPersistentMap (count [_] (.count m)) @@ -148,6 +159,10 @@ (putAll [_ _] (throw (UnsupportedOperationException.))) (clear [_] (throw (UnsupportedOperationException.))) + clojure.lang.IKVReduce + (kvreduce [this f init] + (reduce-kv (fn [acc k _] (f acc k (.valAt this k))) init m)) + clojure.lang.IEditableCollection (asTransient [_] (TransientDelegatedMap. (transient m) fn-entry))) @@ -182,13 +197,15 @@ (-invoke [this k] (-lookup this k)) (-invoke [this k not-found] (-lookup this k not-found)) - ILookup - (-lookup - [this k] - (-lookup this k nil)) - (-lookup - [this k not-found] - (or (some-> ^IMapEntry (-find this k) (val)) not-found)) + ILookup + (-lookup + [this k] + (-lookup this k nil)) + (-lookup + [this k not-found] + (if-let [entry (-find this k)] + (val entry) + not-found)) IMap (-dissoc @@ -251,6 +268,11 @@ IMeta (-meta [_] (-meta m)) + IKVReduce + (-kv-reduce + [this f init] + (reduce-kv (fn [acc k _] (f acc k (-lookup this k))) init m)) + IEditableCollection (-as-transient [_] diff --git a/test/robertluo/fun_map/core_test.cljc b/test/robertluo/fun_map/core_test.cljc index 8bb42ee..4459b88 100644 --- a/test/robertluo/fun_map/core_test.cljc +++ b/test/robertluo/fun_map/core_test.cljc @@ -15,6 +15,18 @@ (is (= 2 (m :a))) (is (= ::not-found (m :c ::not-found)))))) +(deftest nil-and-false-value-test + (testing "nil values are returned correctly with not-found" + (let [m (sut/delegate-map {:a nil :b 1} (fn [_ [k v]] [k v]))] + (is (nil? (get m :a))) + (is (nil? (get m :a :not-found))) ; should return nil, not :not-found + (is (= :not-found (get m :missing :not-found))))) + (testing "false values are returned correctly with not-found" + (let [m (sut/delegate-map {:a false :b true} (fn [_ [k v]] [k v]))] + (is (false? (get m :a))) + (is (false? (get m :a :not-found))) ; should return false, not :not-found + (is (= :not-found (get m :missing :not-found)))))) + (deftest transient-test (letfn [(wrap-m [m f] (-> m (sut/delegate-map (fn [_ [k v]] [k (* 2 v)])) transient f persistent!))] (is (= {} (wrap-m {} identity))) From 3307f386f04f838a1dd12798c92b813d3751e735 Mon Sep 17 00:00:00 2001 From: Robert Luo Date: Mon, 19 Jan 2026 08:20:07 +0800 Subject: [PATCH 6/6] Improve wrapper.cljc: fix race condition, exception handling, cleanup - Fix: -unwrap for Object/nil now throws instead of returning ex-info - Fix: CachedWrapper race condition - move staleness check inside swap! - Add: TracedWrapper print-method for consistency - Cleanup: Remove dead CLJS raw-keys code path - Cleanup: Standardize docstrings across wrapper constructors Co-Authored-By: Warp --- doc/improvements.adoc | 25 +++++++++++++++++++ src/robertluo/fun_map/wrapper.cljc | 40 +++++++++++++++++++----------- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/doc/improvements.adoc b/doc/improvements.adoc index 1686d59..4581f01 100644 --- a/doc/improvements.adoc +++ b/doc/improvements.adoc @@ -160,6 +160,30 @@ CLJS transient fun-maps now support lookups during transient operations. See `doc/concepts.adoc` for details. +== 9. wrapper.cljc Improvements ✓ + +*Status:* DONE + +=== Fixed: `-unwrap` returned exception instead of throwing (Medium priority) + +The `Object` and `nil` implementations of `-unwrap` returned an `ex-info` object instead of throwing it. Now properly throws if these code paths are ever hit (which indicates a bug). + +=== Fixed: `CachedWrapper` race condition (Medium priority) + +Moved focus-fn evaluation and staleness check inside `swap!` to avoid race condition where multiple threads could redundantly recompute cached values. + +=== Added: `TracedWrapper` print method (Low priority) + +Added `print-method` for `TracedWrapper` for consistency with `FunctionWrapper` and `CachedWrapper`. + +=== Cleanup: Removed dead CLJS code (Low priority) + +Removed unreachable `::-raw-seq-fn` metadata check in `raw-keys` CLJS branch. + +=== Cleanup: Standardized docstrings (Low priority) + +Standardized docstring style across `fun-wrapper`, `cache-wrapper`, and `trace-wrapper`. + == Implementation Status [cols="1,3,1"] @@ -174,4 +198,5 @@ See `doc/concepts.adoc` for details. |6 |Add gotchas section |DONE |7 |`Closeable` support for `closeable` |DONE |8 |core.cljc improvements |DONE +|9 |wrapper.cljc improvements |DONE |=== diff --git a/src/robertluo/fun_map/wrapper.cljc b/src/robertluo/fun_map/wrapper.cljc index 93f59ef..e822f71 100644 --- a/src/robertluo/fun_map/wrapper.cljc +++ b/src/robertluo/fun_map/wrapper.cljc @@ -20,11 +20,14 @@ Object (-wrapped? [_ _] false) (-unwrap [this _ k] - (ex-info "Unwrap a common value" {:key k :value this})) + ;; This should never be called since -wrapped? returns false. + ;; If it is called, it indicates a bug in the unwrapping logic. + (throw (ex-info "Bug: attempted to unwrap a non-wrapper value" + {:key k :value this :type (type this)}))) nil (-wrapped? [_ _] false) (-unwrap [_ _ k] - (ex-info "Unwrap a nil" {:key k})) + (throw (ex-info "Bug: attempted to unwrap nil" {:key k}))) clojure.lang.IDeref (-wrapped? [_ m] (not (some-> m meta ::keep-ref))) @@ -42,10 +45,8 @@ (keys m)) :cljs ;; In CLJS we can't easily reference core without circular dep, - ;; and this is only used for error context, so just use keys on underlying map - (if-let [raw-seq-fn (some-> m meta ::-raw-seq-fn)] - (map first (raw-seq-fn)) - (keys m)))) + ;; and this is only used for error context, so just use keys + (keys m))) (defn- wrap-fn-error "Wraps exceptions from function wrapper with context about key and access path." @@ -90,7 +91,7 @@ ) (def fun-wrapper - "construct a new FunctionWrapper" + "Constructs a new FunctionWrapper." ->FunctionWrapper) (defn- check-cycle! @@ -134,11 +135,17 @@ ValueWrapper (-wrapped? [_ _] true) (-unwrap [_ m k] - (let [[val focus-val] @a-val-pair - new-focus-val (if focus-fn (focus-fn m) ::unrealized)] - (if (or (= ::unrealized val) (not= new-focus-val focus-val)) - (first (swap! a-val-pair (fn [_] [(-unwrap wrapped m k) new-focus-val]))) - val))) + ;; Use swap! properly to avoid race condition where multiple threads + ;; could redundantly recompute the value. + ;; Note: focus-fn must be pure since it may be called multiple times + ;; during contention. + (first + (swap! a-val-pair + (fn [[val focus-val]] + (let [new-focus-val (if focus-fn (focus-fn m) ::unrealized)] + (if (or (= ::unrealized val) (not= new-focus-val focus-val)) + [(-unwrap wrapped m k) new-focus-val] + [val focus-val])))))) #?@(:cljs [IPrintWithWriter (-pr-writer @@ -150,7 +157,7 @@ ">>")))])) (defn cache-wrapper - "construct a CachedWrapper" + "Constructs a CachedWrapper." [wrapped focus] (CachedWrapper. wrapped (atom [::unrealized ::unrealized]) focus)) @@ -164,7 +171,7 @@ v))) (def trace-wrapper - "constructs a TraceWrapper" + "Constructs a TracedWrapper." ->TracedWrapper) ;; Fine print the wrappers @@ -178,4 +185,7 @@ (str "<<" (let [v (-> (.a_val_pair o) deref first)] (if (= ::unrealized v) "unrealized" v)) - ">>"))))) + ">>"))) + + (defmethod print-method TracedWrapper [^TracedWrapper o ^java.io.Writer wtr] + (.write wtr (str "<>")))))