使用Common Lisp宏捕获-22情况

And*_*age 7 macros eval common-lisp

通常当我尝试编写一个宏时,我遇到了以下困难:我需要一个传递给宏的表单,然后在生成宏扩展时调用的辅助函数处理之前进行评估.在下面的示例中,我们只关心如何编写宏来发出我们想要的代码,而不是宏本身的无用:

想象一下(忍受我)一个Common Lisp lambda宏的版本,其中只有参数的数量很重要,参数的名称和顺序不是.我们称之为jlambda.它会像这样使用:

(jlambda 2
  ...body)
Run Code Online (Sandbox Code Playgroud)

2返回函数的arity 在哪里.换句话说,这会产生二元运算符.

现在想象一下,给定arity,jlambda产生一个虚拟的lambda列表,它传递给实际的lambda宏,如下所示:

(defun build-lambda-list (arity)
  (assert (alexandria:non-negative-integer-p arity))
  (loop for x below arity collect (gensym)))

(build-lambda-list 2)
==> (#:G15 #:G16)
Run Code Online (Sandbox Code Playgroud)

上述调用的扩展jlambda将如下所示:

(lambda (#:G15 #:16)
  (declare (ignore #:G15 #:16))
  …body))
Run Code Online (Sandbox Code Playgroud)

假设我们需要jlambda宏能够以一个Lisp形式接收arity值,该形式计算为非负整数(而不是直接接收非负整数),例如:

(jlambda (+ 1 1)
  ...body)
Run Code Online (Sandbox Code Playgroud)

(+ 1 1)需要对表单进行求值,然后需要将结果传递给build-lambda-list需要进行评估的结果,并将结果插入到宏扩展中.

(+ 1 1)
=> 2
(build-lambda-list 2)
=> (#:G17 #:18)

(jlambda (+ 1 1) ...body)
=> (lambda (#:G19 #:20)
     (declare (ignore #:G19 #:20))
       …body))
Run Code Online (Sandbox Code Playgroud)

所以这里有一个版本jlambda,当arity直接作为数字提供时,但不是当它作为要评估的表单传递时:

(defun jlambda-helper (arity)
  (let ((dummy-args (build-lambda-list arity)))
  `(lambda ,dummy-args
     (declare (ignore ,@dummy-args))
       body)))

(defmacro jlambda (arity &body body)
  (subst (car body) 'body (jlambda-helper arity)))

(jlambda 2 (print “hello”))  ==> #<anonymous-function>

(funcall *
         'ignored-but-required-argument-a
         'ignored-but-required-argument-b)
==> “hello”
    “hello”

(jlambda (+ 1 1) (print “hello”)) ==> failed assertion in build-lambda-list, since it receives (+ 1 1) not 2
Run Code Online (Sandbox Code Playgroud)

我可以评估(+ 1 1)使用尖点读取宏,如下所示:

(jlambda #.(+ 1 1) (print “hello”)) ==> #<anonymous-function>
Run Code Online (Sandbox Code Playgroud)

但是表单不能包含对词法变量的引用,因为它们在读取时进行评估时不可用:

(let ((x 1))
  ;; Do other stuff with x, then:
  (jlambda #.(+ x 1) (print “hello”))) ==> failure – variable x not bound
Run Code Online (Sandbox Code Playgroud)

我可以引用我传递给的所有正文代码,将其jlambda定义为函数,然后eval是它返回的代码:

(defun jlambda (arity &rest body)
  (let ((dummy-args (build-lambda-list arity)))
  `(lambda ,dummy-args
     (declare (ignore ,@dummy-args))
       ,@body)))

(eval (jlambda (+ 1 1) `(print “hello”))) ==> #<anonymous-function>
Run Code Online (Sandbox Code Playgroud)

但是我无法使用,eval因为它像尖点一样抛出了词汇环境,这是不好的.

所以jlambda必须是一个宏,因为我不希望评估函数体代码,直到通过jlambda扩展建立了适当的上下文; 但它也必须是一个函数,因为我希望在将其传递给生成宏扩展的辅助函数之前评估第一个表单(在此示例中为arity表单).我如何克服这种Catch-22情况?

编辑

在回答@Sylwester的问题时,这里是对上下文的解释:

我写的东西类似于"深奥的编程语言",在Common Lisp中作为DSL实现.这个想法(虽然很愚蠢,但可能很有趣)是强迫程序员尽可能地(我不知道还有多远!),以无点的方式写作.要做到这一点,我会做几件事:

  • 使用curry-compose-reader-mac来提供在CL中以无点样式书写所需的大部分功能
  • 强制执行函数 - 即覆盖CL的默认行为,允许函数为可变参数
  • 而不是使用类型系统来确定函数何时"完全应用"(如在Haskell中),只需在定义函数时手动指定函数的arity.

因此,我需要一个自定义版本lambda来定义这种愚蠢语言中的函数,并且 - 如果我无法弄清楚 - 一个自定义版本funcall和/或apply用于调用这些函数.理想情况下,它们只是略微改变功能的普通CL版本.

这种语言的功能将以某种方式跟踪它的arity.但是,为了简单起见,我想程序本身仍然是一个funcallable CL对象,但真的想避免使用元对象协议,因为它更加混乱对我比宏.

一个可能简单的解决方案是使用闭包.每个函数都可以简单地关闭存储其arity的变量的绑定.调用时,arity值将确定函数应用程序的确切性质(即完整或部分应用程序).如果有必要,关闭可以是"pandoric",以便提供对arity值的外部访问; 可能使用可实现plambdawith-pandoric设在LAMBDA.

通常,我的语言中的函数将表现得如此(可能是错误的伪代码,纯粹是说明性的):

Let n be the number of arguments provided upon invocation of the function f of arity a.
If a = 0 and n != a, throw a “too many arguments” error;
Else if a != 0 and 0 < n < a, partially apply f to create a function g, whose arity is equal to a – n;
Else if n > a, throw a “too many arguments” error;
Else if n = a, fully apply the function to the arguments (or lack thereof).
Run Code Online (Sandbox Code Playgroud)

这个问题与之g相等的事实是:需要像这样创建:a – njlambdag

(jlambda (- a n)
  ...body)
Run Code Online (Sandbox Code Playgroud)

这意味着访问词汇环境是必要的.

Jos*_*lor 6

这是一个特别棘手的情况,因为在运行时没有明显的方法来创建特定数量的参数的函数.如果没有办法做到这一点,那么编写一个带有arity和另一个函数的函数可能最简单,并且将函数包装在一个新函数中,该函数需要提供特定数量的参数:

(defun %jlambda (n function)
  "Returns a function that accepts only N argument that calls the
provided FUNCTION with 0 arguments."
  (lambda (&rest args)
    (unless (eql n (length args))
      (error "Wrong number of arguments."))
    (funcall function)))
Run Code Online (Sandbox Code Playgroud)

一旦你拥有了它,你可以很容易地编写你想要的宏:

(defmacro jlambda (n &body body)
  "Produces a function that takes exactly N arguments and and evalutes
the BODY."
  `(%jlambda ,n (lambda () ,@body)))
Run Code Online (Sandbox Code Playgroud)

它的行为大致与您希望的方式相同,包括让arity成为编译时未知的东西.

CL-USER> (let ((a 10) (n 7))
           (funcall (jlambda (- a n)
                      (print 'hello))
                    1 2 3))

HELLO 
HELLO
CL-USER> (let ((a 10) (n 7))
           (funcall (jlambda (- a n)
                      (print 'hello))
                    1 2))
; Evaluation aborted on #<SIMPLE-ERROR "Wrong number of arguments." {1004B95E63}>.
Run Code Online (Sandbox Code Playgroud)

现在,你可能能够做的东西,调用编译器在运行时,可能间接使用强迫,但不会让函数体能够引用变量在原来的词汇范围,虽然你得到实现错误的参数数量异常:

(defun %jlambda (n function)
  (let ((arglist (loop for i below n collect (make-symbol (format nil "$~a" i)))))
    (coerce `(lambda ,arglist
               (declare (ignore ,@arglist))
               (funcall ,function))
            'function)))

(defmacro jlambda (n &body body)
  `(%jlambda ,n (lambda () ,@body)))
Run Code Online (Sandbox Code Playgroud)

这适用于SBCL:

CL-USER> (let ((a 10) (n 7))
           (funcall (jlambda (- a n)
                      (print 'hello))
                    1 2 3))
HELLO 

CL-USER> (let ((a 10) (n 7))
           (funcall (jlambda (- a n)
                      (print 'hello))
                    1 2))
; Evaluation aborted on #<SB-INT:SIMPLE-PROGRAM-ERROR "invalid number of arguments: ~S" {1005259923}>.
Run Code Online (Sandbox Code Playgroud)

虽然这在SBCL中有效,但我不清楚它是否真的有效.我们正在使用强制来编译一个具有文字函数对象的函数.我不确定这是否便携.


Syl*_*ter 4

注意: 在您的代码中,您使用了奇怪的引号,因此(print \xe2\x80\x9chello\xe2\x80\x9d)实际上不会打印hello变量的\xe2\x80\x9chello\xe2\x80\x9d计算结果,而(print "hello")会执行人们所期望的操作。

\n\n

我的第一个问题是为什么?通常您知道编译时需要多少个参数,或者至少您只是将其设置为多个参数。创建arity 函数只会在使用错误数量的参数作为附加功能的 passwd 时给出错误,并具有使用和朋友n的缺点。eval

\n\n

它无法作为宏来解决,因为您将运行时间与宏扩展时间混合在一起。想象一下这样的用途:

\n\n
(defun test (last-index)\n  (let ((x (1+ last-index)))\n    (jlambda x (print "hello"))))\n
Run Code Online (Sandbox Code Playgroud)\n\n

当评估此形式时,宏将展开,并且在将函数分配给 之前替换内容test。此时x没有任何值,果然宏函数只获取符号,因此结果需要使用该值。lambda是一种特殊形式,因此它在扩展后立即再次扩展jlambda以及使用该函数之前立即再次展开。

\n\n

没有任何词汇发生,因为这是在程序运行之前发生的。它可能发生在加载文件之前compile-file,然后如果加载它将加载所有已经预先扩展了宏的表单。

\n\n

您可以使用compile数据创建函数。它可能和现在一样邪恶,eval所以你不应该将它用于常见任务,但它们存在是有原因的:

\n\n
;; Macro just to prevent evaluation of the body \n(defmacro jlambda (nexpr &rest body)\n  `(let ((dummy-args (build-lambda-list ,nexpr)))\n     (compile nil (list* \'lambda dummy-args \',body))))\n
Run Code Online (Sandbox Code Playgroud)\n\n

所以第一个例子的扩展就变成了这样:

\n\n
(defun test (last-index)\n  (let ((x (1+ last-index)))\n    (let ((dummy-args (build-lambda-list x))) \n      (compile nil (list* \'lambda dummy-args \'((print "hello")))))))\n
Run Code Online (Sandbox Code Playgroud)\n\n

这看起来可行。让我们测试一下:

\n\n
(defparameter *test* (test 10))\n(disassemble *test*)\n;Disassembly of function nil\n;(CONST 0) = "hello"\n;11 required arguments <!-- this looks right\n;0 optional arguments\n;No rest parameter\n;No keyword parameters\n;4 byte-code instructions:\n;0     (const&push 0)                      ; "hello"\n;1     (push-unbound 1)\n;3     (calls1 142)                        ; print\n;5     (skip&ret 12)\n;nil\n
Run Code Online (Sandbox Code Playgroud)\n\n

可能的变化

\n\n

我制作了一个宏,它采用文字数字并从a...创建可在函数中使用的绑定变量。

\n\n

如果您不使用参数,为什么不创建一个宏来执行此操作:

\n\n
(defmacro jlambda2 (&rest body)\n  `(lambda (&rest #:rest) ,@body))\n
Run Code Online (Sandbox Code Playgroud)\n\n

结果接受任意数量的参数并忽略它:

\n\n
(defparameter *test* (jlambda2 (print "hello")))\n(disassemble *test*)\n;Disassembly of function :lambda\n;(CONST 0) = "hello"\n;0 required arguments\n;0 optional arguments\n;Rest parameter <!-- takes any numer of arguments\n;No keyword parameters\n;4 byte-code instructions:\n;0     (const&push 0)                      ; "hello"\n;1     (push-unbound 1)\n;3     (calls1 142)                        ; print\n;5     (skip&ret 2)\n;nil\n\n(funcall *test* 1 2 3 4 5 6 7)\n; ==> "hello" (prints "hello" as side effect)\n
Run Code Online (Sandbox Code Playgroud)\n\n

编辑

\n\n

现在我知道你在做什么,我有一个答案给你。您的初始函数不需要依赖于运行时,因此所有函数确实都有固定的数量,因此我们需要进行柯里化或部分应用。

\n\n
;; currying\n(defmacro fixlam ((&rest args) &body body)\n  (let ((args (reverse args)))\n    (loop :for arg :in args\n          :for r := `(lambda (,arg) ,@body)\n                 :then `(lambda (,arg) ,r)\n          :finally (return r))))\n\n(fixlam (a b c) (+ a b c)) \n; ==> #<function :lambda (a) (lambda (b) (lambda (c) (+ a b c)))>\n\n\n;; can apply multiple and returns partially applied when not enough\n(defmacro fixlam ((&rest args) &body body)\n  `(let ((lam (lambda ,args ,@body)))\n     (labels ((chk (args)\n                (cond ((> (length args) ,(length args)) (error "too many args"))\n                      ((= (length args) ,(length args)) (apply lam args))\n                      (t (lambda (&rest extra-args)\n                           (chk (append args extra-args)))))))\n       (lambda (&rest args)\n         (chk args)))))\n\n(fixlam () "hello") ; ==> #<function :lambda (&rest args) (chk args)>\n\n;;Same but the zero argument functions are applied right away:\n(defmacro fixlam ((&rest args) &body body)\n  `(let ((lam (lambda ,args ,@body)))\n     (labels ((chk (args)\n                (cond ((> (length args) ,(length args)) (error "too many args"))\n                      ((= (length args) ,(length args)) (apply lam args))\n                      (t (lambda (&rest extra-args)\n                           (chk (append args extra-args)))))))\n       (chk \'()))))\n\n(fixlam () "hello") ; ==> "hello"\n
Run Code Online (Sandbox Code Playgroud)\n