宏扩展即使正确扩展也无法编译?

Macro expansion fails to compile even though it expands correctly?

作为练习,我想实现我不久前读过的 Doug Hoytes "Let over Lambda" 中描述的一些宏。
当我开始玩 anaphoric macros 时,我 运行 遇到了一个奇怪的问题。我已经实现了书中描述的 alet 宏,如下所示:

(defmacro a-let
  "Anaphoric let, `this` refers to the last form in body, which should be a 
   function"
  [bindings & body]
  `(let [~'this (atom nil) ~@bindings]
     (reset! ~'this ~(last body))
     ~@(butlast body)
     (fn [& params]
       (apply ~'@this params))))

这编译得很好。但是,如果我尝试在代码中使用它,例如在这个简单的示例中

(a-let [a 1, b 2]
  (fn [] (+ a b)))

Cider 抗议并抛出以下堆栈跟踪错误:

2. Unhandled clojure.lang.Compiler$CompilerException
   Error compiling /home/dasbente/Dokumente/Informatik/Clojure/let-over-lambda.clj at (39:1)
         Compiler.java: 6891  clojure.lang.Compiler/checkSpecs
         Compiler.java: 6907  clojure.lang.Compiler/macroexpand1
         Compiler.java: 6989  clojure.lang.Compiler/analyzeSeq
         Compiler.java: 6773  clojure.lang.Compiler/analyze
         Compiler.java: 6729  clojure.lang.Compiler/analyze
         Compiler.java: 6100  clojure.lang.Compiler$BodyExpr$Parser/parse
         Compiler.java: 6420  clojure.lang.Compiler$LetExpr$Parser/parse
         Compiler.java: 7003  clojure.lang.Compiler/analyzeSeq
         Compiler.java: 6773  clojure.lang.Compiler/analyze
         Compiler.java: 6729  clojure.lang.Compiler/analyze
         Compiler.java: 6100  clojure.lang.Compiler$BodyExpr$Parser/parse
         Compiler.java: 5460  clojure.lang.Compiler$FnMethod/parse
         Compiler.java: 4022  clojure.lang.Compiler$FnExpr/parse
         Compiler.java: 7001  clojure.lang.Compiler/analyzeSeq
         Compiler.java: 6773  clojure.lang.Compiler/analyze
         Compiler.java: 7059  clojure.lang.Compiler/eval
         Compiler.java: 7025  clojure.lang.Compiler/eval
              core.clj: 3206  clojure.core/eval
              core.clj: 3202  clojure.core/eval
              main.clj:  243  clojure.main/repl/read-eval-print/fn
              main.clj:  243  clojure.main/repl/read-eval-print
              main.clj:  261  clojure.main/repl/fn
              main.clj:  261  clojure.main/repl
              main.clj:  177  clojure.main/repl
           RestFn.java: 1523  clojure.lang.RestFn/invoke
interruptible_eval.clj:   87  clojure.tools.nrepl.middleware.interruptible-eval/evaluate/fn
              AFn.java:  152  clojure.lang.AFn/applyToHelper
              AFn.java:  144  clojure.lang.AFn/applyTo
              core.clj:  657  clojure.core/apply
              core.clj: 1965  clojure.core/with-bindings*
              core.clj: 1965  clojure.core/with-bindings*
           RestFn.java:  425  clojure.lang.RestFn/invoke
interruptible_eval.clj:   85  clojure.tools.nrepl.middleware.interruptible-eval/evaluate
interruptible_eval.clj:   55  clojure.tools.nrepl.middleware.interruptible-eval/evaluate
interruptible_eval.clj:  222  clojure.tools.nrepl.middleware.interruptible-eval/interruptible-eval/fn/fn
interruptible_eval.clj:  190  clojure.tools.nrepl.middleware.interruptible-eval/run-next/fn
              AFn.java:   22  clojure.lang.AFn/run


ThreadPoolExecutor.java: 1149  java.util.concurrent.ThreadPoolExecutor/runWorker
   ThreadPoolExecutor.java:  624  java.util.concurrent.ThreadPoolExecutor$Worker/run
               Thread.java:  748  java.lang.Thread/run

1. Caused by clojure.lang.ExceptionInfo
   Call to clojure.core/fn did not conform to spec: In: [0 1] val:
   user/params fails spec: :clojure.core.specs.alpha/local-name at:



[:args :bs :arity-1 :args :varargs :form :sym] predicate:
   simple-symbol?  In: [0 1] val: user/params fails spec:
   :clojure.core.specs.alpha/seq-binding-form at: [:args :bs :arity-1
   :args :varargs :form :seq] predicate: vector?  In: [0 1] val:
   user/params fails spec: :clojure.core.specs.alpha/map-bindings at:
   [:args :bs :arity-1 :args :varargs :form :map] predicate: coll?
   In: [0 1] val: user/params fails spec:
   :clojure.core.specs.alpha/map-special-binding at: [:args :bs
   :arity-1 :args :varargs :form :map] predicate: map?  In: [0 0] val:
   & fails spec: :clojure.core.specs.alpha/arg-list at: [:args :bs
   :arity-n :args] predicate: vector?

   #:clojure.spec.alpha{:problems
                        ({:path
                          [:args :bs :arity-1 :args :varargs :form :sym],
                          :pred clojure.core/simple-symbol?,
                          :val user/params,
                          :via
                          [:clojure.core.specs.alpha/args+body
                           :clojure.core.specs.alpha/arg-list
                           :clojure.core.specs.alpha/arg-list
                           :clojure.core.specs.alpha/binding-form
                           :clojure.core.specs.alpha/binding-form
                           :clojure.core.specs.alpha/local-name],
                          :in [0 1]}
                         {:path
                          [:args :bs :arity-1 :args :varargs :form :seq],
                          :pred clojure.core/vector?,
                          :val user/params,
                          :via
                          [:clojure.core.specs.alpha/args+body
                           :clojure.core.specs.alpha/arg-list
                           :clojure.core.specs.alpha/arg-list
                           :clojure.core.specs.alpha/binding-form
                           :clojure.core.specs.alpha/binding-form
                           :clojure.core.specs.alpha/seq-binding-form],
                          :in [0 1]}
                         {:path
                          [:args :bs :arity-1 :args :varargs :form :map],
                          :pred clojure.core/coll?,
                          :val user/params,
                          :via
                          [:clojure.core.specs.alpha/args+body
                           :clojure.core.specs.alpha/arg-list
                           :clojure.core.specs.alpha/arg-list
                           :clojure.core.specs.alpha/binding-form
                           :clojure.core.specs.alpha/binding-form
                           :clojure.core.specs.alpha/map-binding-form
                           :clojure.core.specs.alpha/map-bindings],
                          :in [0 1]}
                         {:path
                          [:args :bs :arity-1 :args :varargs :form :map],
                          :pred map?,
                          :val user/params,
                          :via
                          [:clojure.core.specs.alpha/args+body
                           :clojure.core.specs.alpha/arg-list
                           :clojure.core.specs.alpha/arg-list
                           :clojure.core.specs.alpha/binding-form
                           :clojure.core.specs.alpha/binding-form
                           :clojure.core.specs.alpha/map-binding-form
                           :clojure.core.specs.alpha/map-special-binding],
                          :in [0 1]}
                         {:path [:args :bs :arity-n :args],
                          :pred clojure.core/vector?,
                          :val &,
                          :via
                          [:clojure.core.specs.alpha/args+body
                           :clojure.core.specs.alpha/args+body
                           :clojure.core.specs.alpha/args+body
                           :clojure.core.specs.alpha/arg-list
                           :clojure.core.specs.alpha/arg-list],
                          :in [0 0]}),
                        :spec
                        #object[clojure.spec.alpha$regex_spec_impl$reify__2436 0x3517c752 "clojure.spec.alpha$regex_spec_impl$reify__2436@3517c752"],
                        :value
                        ([& user/params]
                         (clojure.core/apply @this user/params)),
                        :args
                        ([& user/params]
                         (clojure.core/apply @this user/params))}

                  core.clj: 4739  clojure.core/ex-info
                  core.clj: 4739  clojure.core/ex-info
                 alpha.clj:  689  clojure.spec.alpha/macroexpand-check
                 alpha.clj:  681  clojure.spec.alpha/macroexpand-check
                  AFn.java:  156  clojure.lang.AFn/applyToHelper
                  AFn.java:  144  clojure.lang.AFn/applyTo
                  Var.java:  702  clojure.lang.Var/applyTo
             Compiler.java: 6889  clojure.lang.Compiler/checkSpecs
             Compiler.java: 6907  clojure.lang.Compiler/macroexpand1
             Compiler.java: 6989  clojure.lang.Compiler/analyzeSeq
             Compiler.java: 6773  clojure.lang.Compiler/analyze
             Compiler.java: 6729  clojure.lang.Compiler/analyze
             Compiler.java: 6100  clojure.lang.Compiler$BodyExpr$Parser/parse
             Compiler.java: 6420  clojure.lang.Compiler$LetExpr$Parser/parse
             Compiler.java: 7003  clojure.lang.Compiler/analyzeSeq
             Compiler.java: 6773  clojure.lang.Compiler/analyze
             Compiler.java: 6729  clojure.lang.Compiler/analyze
             Compiler.java: 6100  clojure.lang.Compiler$BodyExpr$Parser/parse
             Compiler.java: 5460  clojure.lang.Compiler$FnMethod/parse
             Compiler.java: 4022  clojure.lang.Compiler$FnExpr/parse
             Compiler.java: 7001  clojure.lang.Compiler/analyzeSeq
             Compiler.java: 6773  clojure.lang.Compiler/analyze
             Compiler.java: 7059  clojure.lang.Compiler/eval
             Compiler.java: 7025  clojure.lang.Compiler/eval
                  core.clj: 3206  clojure.core/eval
                  core.clj: 3202  clojure.core/eval
                  main.clj:  243  clojure.main/repl/read-eval-print/fn
                  main.clj:  243  clojure.main/repl/read-eval-print
                  main.clj:  261  clojure.main/repl/fn
                  main.clj:  261  clojure.main/repl
                  main.clj:  177  clojure.main/repl
               RestFn.java: 1523  clojure.lang.RestFn/invoke
    interruptible_eval.clj:   87  clojure.tools.nrepl.middleware.interruptible-eval/evaluate/fn
                  AFn.java:  152  clojure.lang.AFn/applyToHelper
                  AFn.java:  144  clojure.lang.AFn/applyTo
                  core.clj:  657  clojure.core/apply
                  core.clj: 1965  clojure.core/with-bindings*
                  core.clj: 1965  clojure.core/with-bindings*
               RestFn.java:  425  clojure.lang.RestFn/invoke
    interruptible_eval.clj:   85  clojure.tools.nrepl.middleware.interruptible-eval/evaluate
    interruptible_eval.clj:   55  clojure.tools.nrepl.middleware.interruptible-eval/evaluate
    interruptible_eval.clj:  222  clojure.tools.nrepl.middleware.interruptible-eval/interruptible-eval/fn/fn
    interruptible_eval.clj:  190  clojure.tools.nrepl.middleware.interruptible-eval/run-next/fn
                  AFn.java:   22  clojure.lang.AFn/run
   ThreadPoolExecutor.java: 1149  java.util.concurrent.ThreadPoolExecutor/runWorker
   ThreadPoolExecutor.java:  624  java.util.concurrent.ThreadPoolExecutor$Worker/run
               Thread.java:  748  java.lang.Thread/run

这还不是很奇怪,写宏或类似的东西时会出错。但是,当我使用 macroexpand-1 扩展宏时,生成的代码没有任何问题:

(macroexpand-1 '(a-let [a 1, b 2]
                  (fn [] (+ a b))))
;; => (clojure.core/let [this (clojure.core/atom nil) a 1 b 2] 
;;      (clojure.core/reset! this (fn [] (+ a b))) 
;;                                  (clojure.core/fn [& user/params] (clojure.core/apply (clojure.core/deref this) user/params)))

;; Without namespaces for readability
;; => (let [this (atom nil) a 1 b 2]
;;      (reset! this (fn [] (+ a b)))
;;      (fn [& params] (apply @this params)))

macroexpand-1

之外也可以正常工作
(def f *) ;; => #'user/f
(f) ;; => 1

我不太熟悉 Clojures 宏系统的细节,所以我很乐意了解为什么会出现这种奇怪的行为,因为我对此很迷茫。
提前致谢!

我不确定那条乱七八糟的错误消息是从哪里来的。我不能说我以前见过这样的错误。

当我 运行 它时,我得到:

CompilerException java.lang.RuntimeException: Can't use qualified name as parameter: mandelbrot-redo.seesaw-main.first-main/params, compiling:(C:\Users\slomi\AppData\Local\Temp\form-init395175488607706237.clj:1:1)

那么错误就很明显了。在 ` 引用形式内创建绑定时,它们会自动 namespaced 到当前命名空间。但是,如错误所述,函数参数不能命名空间。

将最后一位更改为:

(fn [& params#]
  (apply ~'@this params#))))

注意 #。这些将 params 变成一个独特的、非命名空间的符号。

现在,它似乎工作正常:

(a-let [a 1, b 2]
       (fn [] (+ a b)))
=>
#object[mandelbrot_redo.seesaw_main.first_main$eval8162$fn__8165
        0x63cf5b7e
        "mandelbrot_redo.seesaw_main.first_main$eval8162$fn__8165@63cf5b7e"]

您也可以使用 promise 代替 atom。它更简洁、更正确,因为您只想设置一次:

(defmacro my-a-let
  "Anaphoric let, `this` refers to the last form in body, which should be a function"
  [bindings & body]
  `(let [~'this (promise)
         ~@bindings]
     (deliver ~'this ~(last body))

     ~@(butlast body)

     (fn [& params#]
       (apply ~'@this params#))))