rewrite-clj

Joel 2025-05-01T19:13:52.157799Z

I’m trying to do a z/remove on a keyword that is a key in a map eg, {:key "fred"} I know I’m looking at :key as z/sexpr returns that. However, I’m getting “cannot remove at top” when I do z/remove Also, if I want to return the key and value is this correct? (-> key-loc z/remove z/remove)?

lread 2025-05-02T10:11:42.280729Z

It still does not add up for me, Joel. How would rewrite-clj restore to a location that does not exist?

lread 2025-05-01T19:33:01.582169Z

Hiya @joel380, please hold while I fire up my REPL.

lread 2025-05-01T19:40:52.583119Z

Ok, let's do some setup:

(require '[rewrite-clj.zip :as z])

(def s "{:key \"fred\"}")

(def zloc (z/of-string s))
If you want to remove :key you'd need to navigate to the :key node first, for example:
(-> zloc z/down z/string)
;; => ":key" 
To remove :key node, navigate to it, then remove it:
(-> zloc z/down z/remove z/root-string)
;; => "{\"fred\"}"

lread 2025-05-01T19:45:13.842439Z

I'm not sure if you made a typo, did you mean? "Also, if I want to remove the key and value is this correct?" You could do this:

(-> zloc
    z/down ;; at :key
    z/right ;; at "fred"
    z/remove ;; at :key after removing "fred"
    z/remove  ;; at {} after removing :key
    z/root-string)
;; => "{}"

👍🏼 1
lread 2025-05-01T19:47:10.452499Z

Does that all make sense and get you unstuck?

Joel 2025-05-01T20:05:26.678209Z

The weird thing, is my zloc says it’s at :key, but I can’t do the z/remove.

Joel 2025-05-01T20:06:24.798859Z

(prn "remove" (-> zloc z/sexpr)) ; => remove :key
(z/remove zloc) ; => "cannot remove at top"

Joel 2025-05-01T20:12:03.267899Z

oh, because I’m doing it within a subedit, why is that a problem?

Joel 2025-05-01T20:17:08.880879Z

because presumably the z/subedit-> makes it become the top i suppose.

lread 2025-05-01T20:31:42.398269Z

Yeah that makes sense to me.

Joel 2025-05-01T20:33:41.542759Z

that’s seems a bit odd though, it’s not really the top, although I don’t understand the significance of being at the top… I couldn’t remove the ns from the top of a file?

lread 2025-05-01T20:34:35.385479Z

Oh. Yeah. I think you are right. Let me go back to my REPL.... please hold.

lread 2025-05-01T20:40:33.608409Z

Oh. So. A subedit will try to preserve your location in the zipper, but you are removing that location, no?

Joel 2025-05-01T20:41:33.509699Z

Yes, I’m removing that key and the following value from the map starting at the location of the key.

lread 2025-05-01T20:42:14.903459Z

What problem is subedit solving for you? Do you need to use it?

Joel 2025-05-01T20:43:06.999819Z

Actually, IDK really. I had done something similar where I was just doing z/replace of multiple locations and it wasn’t working until I told it to use subedit.

Joel 2025-05-01T20:43:44.290619Z

I figured I’d do the same approach for z/remove… I’m removing multiple locations.

lread 2025-05-01T20:44:43.781129Z

Subediting can be useful. If you need to preserve your location in the zipper. Maybe you figured that was useful?

Joel 2025-05-01T20:45:32.056969Z

Yeah, for the replacements, I think the locations were no longer valid unless I used subedit.

Joel 2025-05-01T20:46:35.298519Z

But it looks like remove works without it which I find odd as I’m still using a find-by-position function to find the next spot to edit, and it seems to be working fine.

Joel 2025-05-01T20:47:49.698899Z

I thought subedit was preserving the following locations so I could still find them. But now my mental model of what’s happening is broken 😞

Joel 2025-05-01T20:48:34.619519Z

I’m grasping the positions and iterating through them applying edits to each position.

lread 2025-05-01T20:50:45.747279Z

Have you read through the https://cljdoc.org/d/rewrite-clj/rewrite-clj/1.1.49/doc/user-guide#sub-editing yet?

lread 2025-05-01T20:52:33.536519Z

Subedit tries to preserve the current location. Can totally make sense to use it.

Joel 2025-05-01T20:54:29.893369Z

Doesn’t it preserve all locations?

lread 2025-05-01T21:00:00.657759Z

Well, a zipper wraps a tree of nodes and tracks the current location. As you move through the tree you affect the current location. Does this help?

(-> "(fn [] {:a 1 :b 2})"
    z/of-string ;; location is at (fn ..)
    z/down ;; location is at fn
    z/right ;; location is at []  
    z/right ;; location is at map
    z/down ;; location is at :a
    z/rightmost ;; location is at 2
    z/remove ;; location is at :b 
    z/remove ;; location is at 1
    z/string)
;; => "1"

(-> "(fn [] {:a 1 :b 2})"
    z/of-string ;; location is at (fn ..)
    z/down ;; location is at fn
    z/right ;; location is at []  
    z/right ;; location is at map
    (z/subedit-> z/down z/rightmost z/remove z/remove)
    ;; location is preserved, still at map
    z/string)
;; => "{:a 1}"

Joel 2025-05-01T21:10:33.370799Z

(-> "(fn [] {:a 1 :b 2}) ()"
                z/of-string
                z/remove)
This doesn’t complain, why does subedit?

Joel 2025-05-01T21:11:38.260009Z

(-> "(fn [] {:a 1 :b 2}) ()"
                z/of-string
                (z/subedit->
                 z/remove))
blows up

lread 2025-05-01T21:12:40.275579Z

I think because you are removing the current location? And subedit wants to preserve the current location?

lread 2025-05-01T21:15:29.028009Z

I typically use subedit when it is challenging to restore my location in the tree after making changes down the tree.

Joel 2025-05-01T21:19:11.772849Z

hmm, that’s subtle but makes sense. I need to look at my other usage better to understand why it broke without it (subedit).

lread 2025-05-01T21:19:33.955449Z

The word "sub" in "subedit" is important to remember.

lread 2025-05-01T21:20:23.015129Z

Happy to help if you get stuck.

Joel 2025-05-01T21:24:55.530629Z

i think a key here is that z/remove moves position to the left, but there is no left.

lread 2025-05-01T22:13:26.435259Z

When you parse forms into a zipper there is a :forms node that represents the top-level implicit do.

(-> ":foo :bar :baz"
    z/of-string
    z/up
    z/tag)
;; => :forms
So even when you remove :foo :bar and :baz, you still haven't removed the top level :form:
(-> ":foo :bar :baz"
    z/of-string
    z/rightmost
    z/remove
    z/remove
    z/remove
    z/tag)
;; => :forms
But if we add one more remove you'll see the "remove at top" exception:
(-> ":foo :bar :baz"
    z/of-string
    z/rightmost
    z/remove
    z/remove
    z/remove
    z/remove)
;; => Execution error at rewrite-clj.custom-zipper.core/remove (core.cljc:273).
;;    Remove at top
A subedit does not have the extra :forms node:
(defn my-silly-fn [zloc]
  (try
    (println "tag" (z/tag zloc))
    (println "node" (z/string zloc))
    (println "up" (z/up zloc))
    (z/remove zloc)
    (catch Exception ex
      (println "ex" (ex-message ex))))
  zloc)

(-> ":foo"
    z/of-string
    (z/subedit-> my-silly-fn))
Prints:
tag :token
node :foo
up nil
ex Remove at top
(the remove threw because we were at top within subedit) If we repeat without subediting:
(-> ":foo"
    z/of-string
    my-silly-fn)
Prints:
tag :token
node :foo
up [<forms: :foo> nil]
(the remove did not throw because we were not at top)

lread 2025-05-01T22:15:11.264409Z

Does that help?

Joel 2025-05-01T22:27:56.756099Z

It explains why the behavior differs at “doc level” vs. “subedit level”, but should the behavior differ?

lread 2025-05-01T23:18:12.927909Z

I feel it is acceptable, yes. An alternative that somehow supports locating to a deleted subedit root node would be more confusing than helpful.

lread 2025-05-01T23:18:49.672079Z

But happy to be convinced otherwise if you think of something that makes sense for general use.

Joel 2025-05-02T02:17:47.396299Z

Just for consistency.