Search code examples
syntaxclojure

Clojure equivalent of reagent cursor to update nested map and access new value


I find myself doing a lot of (get-in (swap! nested-map update-in [:a :b] update-fn) [:a :b]) to update a new value and then access it atomically. It seems a bit clunky to me - is there a more elegant way of doing this? reagent has the cursor idea which works nicely as once you define the cursor the above just becomes (swap! cursor update-fn).

I found https://github.com/rlewczuk/clj-cursor which seems to be what I want but maybe overkill?

Thanks,


Solution

  • Implementing your own cursor based on the clojure.lang.IAtom interface is fairly straight-forward:

    (defn my-cursor [target-atom path]
      (reify clojure.lang.IAtom
        (swap [_this f] (get-in (swap! target-atom update-in path f) path))))
    

    Note that clojure.lang.IAtom may be considered an implementation detail of Clojure so whether you want to depend on it is up to you.

    Here is an example:

    (def nested-map (atom {}))
    (defn update-fn [x] (inc (or x 0)))
    
    (def c (my-cursor nested-map [:a :b]))
    
    (swap! c update-fn)
    ;; => 1
    
    (deref nested-map)
    ;; => {:a {:b 1}}
    

    Implementing IDeref may also be useful:

    (defn my-cursor [target-atom path]
      (reify clojure.lang.IAtom
        (swap [_this f] (get-in (swap! target-atom update-in path f) path))
        clojure.lang.IDeref
        (deref [_this] (get-in (deref target-atom) path))))
    

    Even if you don't have to implement the other methods of the interfaces, it is nevertheless a good idea.

    A previous version of this answer used proxy but reify is probably better.