1

I'm writing a macro that looks through the metadata on a given symbol and removes any entries that are not keywords, i.e. the key name doesn't start with a ":" e.g.

(meta (var X))  ;; Here's the metadata for testing...
=>
{:line 1,
 :column 1,
 :file "C:\\Users\\Joe User\\AppData\\Local\\Temp\\form-init11598934441516564808.clj",
 :name X,
 :ns #object[clojure.lang.Namespace 0x12ed80f6 "thic.core"],
 OneHundred 100,
 NinetyNine 99}

I want to remove entryes "OneHundred" and "NinetyNine" and leave the rest of the metadata untouched.

So I have a bit of code that works:

    (let [Hold# (meta (var X))]  ;;Make a copy of the metadata to search.
      (map (fn [[kee valu]]      ;;Loop through each metadata key/value.
        (if
          (not= \: (first (str kee)))  ;; If we find a non-keyword key,
          (reset-meta! (var X) (dissoc (meta (var X)) kee))  ;; remove it from X's metadata.
          )
        )
       Hold#  ;;map through this copy of the metadata.
      )
    )

It works. The entries for "OneHundred" and "NinetyNine" are gone from X's metadata.

Then I code it up into a macro. God bless REPL's.

(defmacro DelMeta! [S]  
  `(let [Hold# (meta (var ~S))] ;; Hold onto a copy of S's metadata.
     (map                       ;; Scan through the copy looking for keys that DON'T start with ":"
       (fn [[kee valu]]
         (if                    ;; If we find metadata whose keyname does not start with a ":"
           (not= \: (first (str kee)))
           (reset-meta! (var ~S) (dissoc (meta (var ~S)) kee))  ;; remove it from S's metadata.
           )
         )
       Hold#        ;; Loop through the copy of S's metadata so as to not confuse things.
       )
     )
  )

Defining the macro with defmacro works without error.
macroexpand-1 on the macro, e.g.

(macroexpand-1 '(DelMeta! X))

expands into the proper code. Here:

(macroexpand-1 '(DelMeta! X))
=>
(clojure.core/let
 [Hold__2135__auto__ (clojure.core/meta (var X))]
 (clojure.core/map
  (clojure.core/fn
   [[thic.core/kee thic.core/valu]]
   (if
    (clojure.core/not= \: (clojure.core/first (clojure.core/str thic.core/kee)))
    (clojure.core/reset-meta! (var X) (clojure.core/dissoc (clojure.core/meta (var X)) thic.core/kee))))
  Hold__2135__auto__))

BUT!!!

Actually invoking the macro at the REPL with a real parameter blatzes out the most incomprehensible error message:

(DelMeta! X)  ;;Invoke DelMeta! macro with symbol X.

Syntax error macroexpanding clojure.core/fn at (C:\Users\Joe User\AppData\Local\Temp\form-init11598934441516564808.clj:1:1).
([thic.core/kee thic.core/valu]) - failed: Extra input at: [:fn-tail :arity-1 :params] spec: :clojure.core.specs.alpha/param-list
(thic.core/kee thic.core/valu) - failed: Extra input at: [:fn-tail :arity-n :params] spec: :clojure.core.specs.alpha/param-list

Oh, all-powerful and wise Clojuregods, I beseech thee upon thy mercy. Whither is my sin?

LionelGoulet
  • 557
  • 2
  • 13
  • 1
    I started writing an answer, but stopped, because it's really unclear on what exactly you try to achieve. Some hints: I don't get the same exception, instead I get an exception telling me that 'kee' doesn't exist -- you want to use `kee#` (and `valu#` if you want use it). Read up on variable capture, e.g. in the [Clojure for the Brave and True](https://www.braveclojure.com/writing-macros/). Second, you're calling `map` when all you want is a side-effect, which is clearer expressed by using `doseq`. But the main question is why would you have non-keyword meta data in the first place? – schaueho Feb 02 '21 at 15:54
  • `map` is wrong here, but so is `doseq`, because two updates will clobber each other. Instead use `reduce` or `into`/`for` to build up a new map functionally, and then set it once. – amalloy Feb 02 '21 at 17:33
  • I find the ending line highly entertaining haha – Aaron Bell Feb 03 '21 at 00:34

1 Answers1

3

You don't need a macro here. Also, you are misunderstanding the nature of a Clojure keyword, and the complications of a Clojure Var vs a local variable.

Keep it simple to start by using a local "variable" in a let block instead of a Var:

(ns tst.demo.core
  (:use tupelo.core tupelo.test))

(dotest
  (let [x  (with-meta [1 2 3] {:my "meta"})
        x2 (vary-meta x assoc :your 25 'abc :def)
        x3 (vary-meta x2 dissoc 'abc )]
    (is= x  [1 2 3])
    (is= x2 [1 2 3])
    (is= x3 [1 2 3])

    (is= (meta x)  {:my "meta"})
    (is= (meta x2) {:my "meta", :your 25, 'abc :def})
    (is= (meta x3) {:my "meta", :your 25}))

So we see the value of x, x2, and x3 is constant. That is the purpose of metadata. The 2nd set of tests shows the effects on the metadata of using vary-meta, which is the best way to change the value.

When we use a Var, it is not only a global value, but it is like a double-indirection of pointers in C. Please see this question:

This answer also clarifies the difference between a string, a symbol, and a keyword. This is important.

Consider this code

(def ^{:my "meta"} data [1 2 3])
(spyx data)
(spyx-pretty (meta (var data)))

and the result:

data => [1 2 3]

(meta (var data)) => 
    {:my "meta",
     :line 19,
     :column 5,
     :file "tst/demo/core.cljc",
     :name data,
     :ns #object[clojure.lang.Namespace 0x4e4a2bb4 "tst.demo.core"]}

(is= data [1 2 3])
(is= (set (keys (meta (var data))))
  #{:my :line :column :file :name :ns})

So we have added the key :my to the metadata as desired. How can we alter it? For a Var, use the function alter-meta!

(alter-meta! (var data) assoc :your 25 'abc :def)
(is= (set (keys (meta (var data))))
  #{:ns :name :file 'abc :your :column :line :my})

So we have added 2 new entries to the metadata map. One has the keyword :your as key with value 25, the other has the symbol abc as key with value :def (a keyword).

We can also use alter-meta! to remote a key/val pair from the metadata map:

(alter-meta! (var data) dissoc 'abc )
(is= (set (keys (meta (var data))))
  #{:ns :name :file :your :column :line :my})

Keyword vs Symbol vs String

A string literal in a source file has double quotes at each end, but they are not characters in the string. Similarly a keyword literal in a source file needs a leading colon to identify it as such. However, neither the double-quotes of the string nor the colon of the keyword are a part of the name of that value.

Thus, you can't identify a keyword by the colon. You should use these functions to identify different data types:

the above are from the Clojure CheatSheet. So, the code you really want is:

(defn remove-metadata-symbol-keys
  [var-obj]
  (assert (var? var-obj)) ; verify it is a Var
  (doseq [k (keys (meta var-obj))]
    (when (not (keyword? k))
      (alter-meta! var-obj dissoc k))))

with a sample:

(def ^{:some "stuff" 'other :things} myVar [1 2 3])
(newline) (spyx-pretty (meta (var myVar)))

(remove-metadata-symbol-keys (var myVar))

(newline) (spyx-pretty (meta (var myVar)))

and result:

(meta (var myVar)) => 
{:some "stuff",
 other :things,          ; *** to be removed ***
 :line 42,
 :column 5,
 :file "tst/demo/core.cljc",
 :name myVar,
 :ns #object[clojure.lang.Namespace 0x9b9155f "tst.demo.core"]}


(meta (var myVar)) =>   ; *** after removing non-keyword keys ***
{:some "stuff",
 :line 42,
 :column 5,
 :file "tst/demo/core.cljc",
 :name myVar,
 :ns #object[clojure.lang.Namespace 0x9b9155f "tst.demo.core"]}

The above code was all run using this template project.

Alan Thompson
  • 29,276
  • 6
  • 41
  • 48
  • Alan Thompson: you've answered my questions before and I guess I expected you to do so again here. And you did! 600 words! :) Thank you very much. Let me plow through it, try your samples out, and get back to you. Gold stars, Obi-wan. – LionelGoulet Feb 03 '21 at 13:27
  • OK. Read it. It took me a while, some months ago, to grok the Symbol -> var -> value chain. I often imagined Clojure was using some double-indirection "under the hood." Thanks for confirming that. My beginner knowledge of Clojure did not include the 'keys' function or the 'keyword?' function. Let me play with it and I'll get back with further questions, or hopefully not. :) – LionelGoulet Feb 03 '21 at 14:20