当作为函数参数传递时,如何停止评估lisp形式



我正在学习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的这种评估?

你不能写这个函数;它必须是一个宏操作符。如果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))

创建函数,编译并保存在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
(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的回答指出,有一个标准函数已经为我们完成了大部分工作:compile。它非常通用,但重要的部分是它有一个名称和一个可选的函数定义。函数定义可以是一个lambda表达式,它将被强制转换为一个函数(如上所述),但也会被编译(因为上面的简单强制转换可能不会编译函数)并将其存储为名称的函数定义,如果name为非nil。这意味着compile将完成

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

,附带编译函数的额外好处。Rainer的答案展示了如何使用它,我认为它是这个问题的最优雅的解决方案。

最新更新