简体   繁体   中英

Can't get Clojure macro to execute without expansion error

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

(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, eg

(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?

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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM