1

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!

1 Answers1

1

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#))))
Carcigenicate
  • 43,494
  • 9
  • 68
  • 117
  • Huh, I probably need to look into why my error messages are as weird as they are. Your solution works perfectly. I guess there is still quite a lot I need to learn about Clojure macros as I was not aware of the `#` for symbol names. In any case thanks. I learned something today! – Tobias Möhring Jul 22 '18 at 12:28
  • 1
    @TobiasMöhring Ya, I have no idea how you got that. I dont use `clojure.spec`, so I'm not used to reading such messages, but I'm grasping at straws to even find a relevant part in that error. `#:clojure.spec.alpha{:problems ({:path [:args :bs :arity-1 :args :varargs :form :sym], :pred clojure.core/simple-symbol?,` kind of seems like it's hinting that that, but it's not at all obvious. Oh, and I added a bit at the bottom. – Carcigenicate Jul 22 '18 at 12:35
  • I've actually never ran into a `clojure.spec` error either but I felt like the error messages I receive are a bit useless for a while now so i might have broken something at some point with my current installation. Also, thanks for the suggestion. However it is kind of required for some of the macros a bit later in the book that `this` can be rebound to other functions. Later in the book this is used to model a state machine which rebinds `this` depending on it's current state. Not the most usefull pattern but it's more of a exercise in writing macros in Clojure to me anyway. – Tobias Möhring Jul 22 '18 at 13:11