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