【问题标题】:How to stop evaluating lisp form when passed as function parameter?作为函数参数传递时如何停止评估lisp形式?
【发布时间】:2014-06-25 21:24:07
【问题描述】:

我正在学习 Lisp。现在我正在尝试创建一个函数,该函数将一些有效的 Lisp 形式作为参数,并返回一个在调用时执行 Lisp 形式的函数。例如:

(defun fn (name action)
  (setf (symbol-function name)
        #'(lambda () action)))

当我通过说(+ 4 5 6) 时,该函数会以特定名称创建,并在调用时返回总和。

(fn 'add (+ 4 5 6))
(add) ==> 15

但如果我调用(fn 'error (assert (= 2 3)),则会抛出错误(= 2 3) must evaluate to a non-NIL value.,并且不会创建名为error 的函数。

当作为函数参数传递时,如何停止对 assert 的评估?

【问题讨论】:

  • 注意error是一个Common Lisp函数,你不应该重新定义它。
  • @PauloMadeira 这只是一个示例代码,用于理解和试验这些概念。
  • 我知道,但我觉得这个评论是必要的,至少对后代来说是这样。如果您重新定义error,即使只是为了进行实验,您应该期望与您的 Common Lisp 实现的其余交互非常不稳定,至少可以这么说。您最好尝试使用不命名 CL 函数的符号。除非您对可能发生的事情感到好奇,否则这可能是一种教学探索。
  • 谢谢@PauloMadeira 我明白你的意思了。

标签: lisp common-lisp


【解决方案1】:

你不能写这个函数;它必须是宏运算符。如果fn是一个函数,那么调用:

(fn 'add (+ 4 5 6))

计算参数(+ 4 5 6),将其减少到值15。函数接收15,而不是表达式。我们可以通过引用代码来“解决”这个问题:

(fn 'add '(+ 4 5 6))

但是我们遇到了代码不与词法环境交互的问题。例如,这是行不通的,因为xfn 中不可见:

(let ((x 40)) (fn 'add '(+ x 2)))

要创建一个在适当环境中计算 (+ x 2) 的函数,我们必须在同一词法范围内使用 lambda 运算符:

(let ((x 40)) (lambda () (+ x 2)))

您的fn 运算符可以写成语法糖,生成lambda(没有任何名称):

(defmacro fn (expr) `(lambda () ,expr))

现在我们可以写了:

(let ((x 40)) (fn (+ x 2))) ;; returns a function which returns 42

做指定的事情:

(defmacro fn (name expr) `(setf (symbol-function ',name) (lambda () ,expr)))

但是,这是一个非常糟糕的主意;我们在函数中引入了令人讨厌的全局副作用。更好的“命名 fn”可能是在某些形式上为函数引入词法绑定的方法。也就是可以这样使用:

(fn (foo (+ x 2)) (foo))
             ;;  ^^^^^^  foo is a lexical function in this scope
             ;;          denoting the function (lambda () (+ x 2))

可以这样做:

(defmacro fn ((name expr) &rest forms)
   `(flet ((,name () ,expr)) ,@forms)))

或者,如果你想把名字作为变量绑定而不是函数绑定,那么用法就是(fn (foo (+ x 2)) (funcall foo)):

(defmacro fn ((name expr) &rest forms)
  `(let ((,name (lambda () ,expr))) ,@forms))

【讨论】:

  • 感谢卡兹您的意见。我现在还有一个疑问:什么是“词法函数”,在什么情况下我可能需要它们?
  • @Santanu 词法(或“本地”)函数由fletlabels 引入(区别在于labels 让定义的函数体“看到”自己和彼此,允许对于自我和相互递归)。当您想将函数的任务分解为更小的函数时,词法函数很有用,但这些函数需要访问父函数中的局部变量。
  • 只是一个想法 - Lisp 中的这种词法作用域可以用于实现更紧密的封装,例如类似于 C 中的static 作用域吗?
  • @Santanu,C 没有闭包,所以 C 中的 static 说明符只是它们的替代品。使用闭包,您可以做比使用 static 说明符更有趣的事情,而且方式更自然。
  • @Mark 但是在 C 中,函数本身可以通过函数指针用作变量。它不是与闭包等效的概念吗?我们可以将函数指针作为参数传递给函数,也可以将其作为函数的返回类型。
【解决方案2】:

创建函数,编译并存储在name下:

(defun fn (name action)
  (compile name
           `(lambda () ,action)))

让我们试试吧:

CL-USER 13 > (fn 'add '(+ 4 5 6))
ADD
NIL
NIL

CL-USER 14 > (add)
15

【讨论】:

  • +1 我总是忘记compile 将设置name 的定义。这是一个非常好的和干净的解决方案。
  • 这个解决方案对我来说真的很新鲜。在搜索网上我总是得到(setf (symbol-function name) ...。所以我心中有个疑问:(setf (symbol-function name) ...(compile name ... 有什么区别?什么时候用哪个?
  • @Santanu:这会创建函数,编译并存储它。 setf symbol-function 只是存储它。
  • @RainerJoswig:好的,所以我可以假设编译比setf symbol-function 提高了执行函数的性能。
  • 刚才我在On Lisp 中发现了这个,并认为应该在这里提及它。 However, calling compile explicitly is a drastic measure, comparable to calling eval, and should be viewed with the same suspicion. ... Calling compile is not a routinely used programming technique-it's an extremely rare one. So beware of doing it unnecessarily. 参考:2.9 Compilation
【解决方案3】:
(defun fn (name action) (setf (symbol-function name) #'(lambda () action)))

(fn 'add (+ 4 5 6)) (add) ==> 15

这不会以相同的方式处理 add(+ 4 5 6)。你引用一个(因为你想要一个符号),而不是另一个,即使你想要一个列表。要获得您想要的行为,您需要定义一个宏以便您可以防止评估并将表单放入函数中,或者构建一个您强制为函数的列表。宏观方法:

(defmacro make-fn (name form)
  `(setf (symbol-function ',name) #'(lambda () ,form)))
CL-USER> (make-fn add (+ 4 5 6))
#<FUNCTION (LAMBDA ()) {1002D48D09}>
CL-USER> (add)
15
CL-USER> (make-fn err (assert (= 2 3)))
#<FUNCTION (LAMBDA ()) {1002E11359}>
CL-USER> (err)
; Evaluation aborted on #<SIMPLE-ERROR "The assertion ~S failed." {1002E24951}>.

函数和强制方法:

(defun make-fn2 (name form)
  (setf (symbol-function name) (coerce (list 'lambda () form) 'function)))
CL-USER> (make-fn2 'add '(+ 4 5 6))
#<FUNCTION (LAMBDA ()) {1004566CB9}>
CL-USER> (add)
15
CL-USER> (make-fn2 'err '(assert (= 2 3)))
#<FUNCTION (LAMBDA ()) {100298D2F9}>
CL-USER> (err)
; Evaluation aborted on #<SIMPLE-ERROR "The assertion ~S failed." {10029CF441}>.

现在,这些方法可以正常工作,但Rainer Joswig's answer 指出有一个标准函数已经为我们完成了大部分工作:compile。它非常通用,但重要的部分是它需要一个名称和可选的函数定义。函数定义可以是 lambda 表达式,它会被强制转换为函数(如上),但也被编译(因为上面的简单强制可能无法编译函数)并将其存储为名称的函数定义,如果名称是非零。这意味着 compile 将完成

的所有工作
(setf (symbol-function name) (coerce (list 'lambda () form) 'function))

对你来说,编译函数还有额外的好处。 Rainer 的回答展示了如何使用它,我认为这是解决这个问题的最优雅的方法。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-02-21
    • 1970-01-01
    • 2016-04-20
    • 2020-11-14
    • 2023-03-10
    • 1970-01-01
    • 1970-01-01
    • 2014-03-13
    相关资源
    最近更新 更多