Search code examples
clojuremacrosmetaprogrammingcider

Macro expansion fails to compile even though it expands correctly?


As practice I wanted to implement some of the macros described in Doug Hoytes "Let over Lambda" which I've read a while ago.
When I started to play around with anaphoric macros I ran into a weird problem. I've implemented the alet macro described in the book as follows:

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

This compiles fine. However, if I try to use it in code, for instance in this simple example

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

Cider protests and throws an error with the stack trace below:

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

This is all still not really weird, mistakes happen when writing macros or something like that. However, when I expanded the macro using macroexpand-1, the resulting code made no problems whatsoever:

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

Which also works perfectly fine outside of macroexpand-1

(def f *) ;; => #'user/f
(f) ;; => 1

I'm not that familiar with the details of Clojures macro system so I'd be happy to be enlightened as to why this weird behaviour occurs because I am pretty lost on this one.
Thanks in advance!


Solution

  • I'm not sure where that mess of an error message came from. I can't say I've ever seen an error like that before.

    When I run it, I get:

    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)
    

    Then the error is obvious. When creating bindings inside of a ` quoted form, they're automatically namespaced to the current namespace. Function parameters can't be namespaced however, as the error says.

    Change the last bit to:

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

    Note the #. Those turn params into a unique, non-namespaced symbol.

    Now, it appears to work fine:

    (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"]
    

    You can also use a promise instead of an atom. It's a little neater, and more correct, since you only want to set it once:

    (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#))))