想象一下(请耐心等待),Common Lisp 的 lambda 宏的一个版本,只有参数数量很重要,参数的名称和顺序并不重要。让我们称之为 jlambda。它将被使用如下:
(jlambda 2
...body)
其中2
是返回函数的元数。换句话说,这将产生一个二元运算符。
现在想象一下,给定元数,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)
上述对
jlambda
的调用扩展将如下所示:(lambda (#:G15 #:16)
(declare (ignore #:G15 #:16))
…body))
假设我们需要jlambda
宏能够接收一个Lisp表达式作为其arity值,该表达式会评估为非负整数(而不是直接接收非负整数),例如:
(jlambda (+ 1 1)
...body)
表达式
(+ 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))
这里有一个版本的jlambda
,当输入参数数量以数字形式直接提供时能够工作,但是当数量作为一个需要被求值的表单传递时不能工作:
(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
我可以使用井号点读宏来评估(+ 1 1)
,如下所示:
(jlambda #.(+ 1 1) (print “hello”)) ==> #<anonymous-function>
但是表单不能包含对词法变量的引用,因为在读取时评估时它们不可用:
(let ((x 1))
;; Do other stuff with x, then:
(jlambda #.(+ x 1) (print “hello”))) ==> failure – variable x not bound
我可以引用传递给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>
但我不能使用
eval
,因为像sharp-dot一样,它会抛弃词法环境,这是不好的。所以
jlambda
必须是一个宏,因为在jlambda
扩展建立正确上下文之前,我不希望函数体代码被评估; 但它也必须是一个函数,因为我希望先评估第一个表格(在本例中是arity form),然后将其传递给生成宏扩展的辅助函数。如何克服这个困境?编辑
回应@Sylwester的问题,以下是上下文的解释:
我正在编写类似于Common Lisp中DSL实现的“奇特编程语言”。这个想法(尽管愚蠢但有潜力很有趣)是尽可能迫使程序员仅使用point-free style编写。为此,我将做几件事:
使用curry-compose-reader-macros提供大部分编写点自由风格的功能,以便在CL中进行编程。 强制执行函数的元数——即覆盖允许函数变异态的CL的默认行为。 不像Haskell一样使用类型系统来确定函数何时被“完全应用”,只需在定义时手动指定函数的元数。
所以我需要一个自定义版本的
lambda
来定义这种愚蠢语言中的函数,并且——如果我无法理解那个版本——自定义版本的funcall
和/或apply
来调用那些函数。理想情况下,它们将只是正常CL版本的皮肤,稍微改变一下功能。这种语言中的函数将以某种方式跟踪其元数。然而,为了简单起见,我希望该过程本身仍然是可调用的CL对象,但真的很想避免使用元对象协议,因为它比宏更加令人困惑。
一个可能简单的解决方案是使用闭包。每个函数可以简单地关闭绑定一个变量,该变量存储其元数。当调用时,元数值将确定函数应用的确切性质(即完整或部分应用)。如果必要,闭包可以是“全局的”,以便提供对元数值的外部访问; 可以使用 Let Over Lambda 中的 plambda
和 with-pandoric
实现。
通常情况下,我的语言中的函数将表现如下(潜在有缺陷的伪代码,仅为说明):
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).
g
的元数等于a - n
是导致jlambda
问题的原因所在:需要像这样创建g
:
(jlambda (- a n)
...body)
这意味着访问词法环境是必要的。
(declare (ignore ,@ dummy-args))
的示例代码,我认为重点在于该函数具有特定的arity,但不使用其参数。有点像CL的constantly,它可以获取任意数量的参数,但始终返回相同的值,这些函数将获取一定数量的参数,但始终评估相同的主体。 - Joshua Taylor