使用 defun 以编程方式构建 Common Lisp 函数

Programmatically constructing a Common Lisp function with defun

我想使用 defun 定义一个函数,但不是在顶层。 (函数名称和主体需要根据一些用户输入来构造。)下面说明了基本的 objective:

(let ((name 'fn1)
      (body "abc"))
  (eval `(defun ,name ()
           ,body))
  (push name *function-names*))

1) 这行得通,但是没有 eval 怎么办?

2) 目前,我正在使用

编译许多这样的函数
(loop for fn-name in *function-names* do
      (setf (symbol-function fn-name)
        (compile nil (symbol-function fn-name))))

但是有没有更好的方法只使用像 `(compile ,fn-name) 这样的 fn-name 来避免在函数体包含对函数的递归调用时出现 SBCL 编译器警告?

在没有 DEFUN 的情况下这样做会有点问题。我认为那是 ANSI Common Lisp 的一部分,不太好。

如果您查看 Common Lisp 的实现,它们会将 DEFUN 形式扩展为特定于实现的结构。我们经常会看到类似 命名的 lambda.

SBCL:

(defun foo ()
  (foo))

->

(SB-INT:NAMED-LAMBDA FOO ()
  (BLOCK FOO (FOO)))

计算结果为:

#<FUNCTION FOO {1003271FFB}>

不幸的是,命名的 lambda 不是 ANSI CL 中的概念。

为此,您需要:

  • 构造 DEFUN 形式
  • 评估它
  • 可选择编译它

要获得类似的效果,我们可以创建一个文件,编译并加载它。

或者我们可以尝试不使用 DEFUN。

  • 我们需要确保全局名称不是宏
  • 然后我们构造一个lambda表达式
  • 然后我们编译那个表达式

示例:递归函数:

(compile 'my-fn '(lambda () (my-fn))

现在我们可能仍然会在编译 lambda 时收到有关未定义函数的警告。我们如何提供名称? LABELS 会这样做:

(compile 'my-fn
         (lambda ()
           (labels ((my-fn ()
                      (my-fn)))
             (my-fn))))

LABELS 也会像 DEFUN 一样设置 BLOCK。

当我们现在调用全局 MY-FN 时,它会调用局部 MY-FN,然后局部 MY-FN 可以递归调用自身。

让我们看看是否有更多替代方法...

  • 也可以使用带有 COMPILE 的普通 LAMBDA 并首先声明函数。这也可能会抑制编译警告。

我对 "why" 有点困惑,你正试图用 defun 来做到这一点,但如果你希望用户能够 定义 他们的自己的函数然后调用它们,有几种方法可以做到这一点。

1) 如果你想像普通的 defun 函数那样计算它,那么你可以做类似

的事情
(loop for f in *my-functions-names* and
      for a in *my-functions-args* and
      for b in *my-functions-bodies*
  do (setf (symbol-function f) (compile `(lambda ,a ,b))))

然后用户可以在执行时编译他们的函数,看起来它一直都在那里。

2) 创建 lambdas 的散列 table 和 funcall(或 apply)它们的函数:

(defparameter *user-functions* (make-hash-table))

(defun def-user-fun (f-name f-args f-body &optional (ns *user-functions*))
  (setf (gethash f-name ns) (compile nil `(lambda ,f-args ,f-body))))

(defun call-user-fun (f-name args &optional (ns *user-functions*))
  (funcall (gethash f-name *user-funcations) f-args))

这将允许用户定义他们想要的任何功能,这些功能将在当前环境中进行评估。

3) 如果这只是为了节省您编译自己的代码的时间,那么您可以按照您所做的去做,但使用 defmacro 和循环语句。

更新

既然你需要递归,那么你可以做一些类似于@rainerjoswig 在 (lambda (args..) (labels ((recurse ...)) (recurse ...))) 中建议的事情。尽管 "nice" 看起来不像命令式的方式,但这在 Lisp 中是惯用的。事实上,SBCL compiler and garbage collector are tuned specifically for this kind of recursion. If you want to know about Common Lisp optimizations you can depend on, you should read the standard.

(compile 'fname) == (setf (symbol-function 'fname) (compile nil (symbol-function 'fname)))。至少可能不是你想的那样。在我看来,您对两件事感到困惑,这两件事都是 lisp "backend" 神秘机器的一部分。

首先,symbol-function 不是 求值器,它只是让 lisp reader 知道 this symbol 是一个 function 符号,应该通过 function 环境访问,而不是当前的词法环境,也就是说,像访问变量一样访问函数但在不同的环境或名称空间中。我敢打赌,在后台 labels 是如何扩展的就是这个函数。

其次,compile也不行。您可以向它传递一个 symbol-function 名称,它访问 stored 函数定义,编译它,然后使用编译定义的旧定义,(compile 'my-func),您可以向它传递一个将编译的 lambda 表达式,(compile nil (lambda (args...) body)),或者,最后,您可以同时传递一个名称和一个定义,它将编译函数存储为该函数变量。

所以是的,在某种意义上它确实扩展到 setf 但不是你的特定 setf 因为你不应该做 (compile nil (symbol-function 'fname)).

至于允许递归,通过解构一般的 defun 形式来扩展 @rainerjoswig 很简单

(def-user-func count-down (val) 
  (if (> val 0)
    (progn (print val) (count-down (1- val)))
    nil))

使用像

这样的宏
(defmacro def-user-func (name args &body body)
  `(compile ',name (lambda (,@args)
                     (labels ((,name (,@args)
                                ,@body))
                        (,name ,@args)))))

由于 lisp 在 letlabels 和其他类似语句中隐藏变量名称的方式,这将非常有效。 "outside" 世界只看到一个函数,而函数只看到它自己的 labels 函数。当 ,@body 扩展时,它会在 labels 的上下文中扩展,并且由于我们复制了名称,它会正常工作。