有没有办法在 Lisp 中声明局部变量(避免 let)?

Posted

技术标签:

【中文标题】有没有办法在 Lisp 中声明局部变量(避免 let)?【英文标题】:Is there a way to declare local variables in Lisp (avoiding let)? 【发布时间】:2013-06-27 22:59:16 【问题描述】:

我喜欢 Lisp,但我觉得它令人讨厌的一件事是它嵌套太多。

在命令式编程语言中,我可以通过使用中间值来中断长表达式,例如:

int x = someFunctionCall() ? someOtherFunctionCall() : 42;
int y = myUnterminableNameFunction(x);

而不是

int x = myUnterminableNameFunction(someFunctionCall() ? someOtherFunctionCall() : 42);

这也可以在 Lisp 中完成,但据我所知,只能使用 letlet 引入了额外的嵌套级别,我宁愿避免这样做。

我不想争论这种观点,而是想找到一种方法在单个非嵌套函数/宏调用中声明局部变量。类似declare_local 的内容如下:

(defun my_function (a b)
   (declare_local x (if (some_function_call) (some_other_function_call) 42))
   (my_unterminable_name_function x))

如果它不存在,是否可以通过一个聪明的宏来实现,而不影响性能?

【问题讨论】:

我觉得奇怪的是你喜欢 lisp 但不喜欢括号嵌套!这是野兽的本性,不是吗? 我喜欢 Lisp 的主要是宏。括号使宏更容易并且因此被容忍:)我仍在寻找保持括号稀疏的方法。 我不清楚declare_local 对你有什么好处。是不是函数末尾少了一个括号? 基本上,它避免了一堆嵌套的let内部函数。因此,它节省了缩进,是的,节省了那些令人困惑的右括号。 【参考方案1】:

您可以使用let* 表单进行顺序绑定。

(let* ((x (if (some-function-call)
             (some-other-call)
             42))
       (y (my-unterminable-name-function x)))
   (bla-bla-bla)
   ...)

它确实嵌套,但不是那么多。

截至declare-local,它必须由一些外部宏处理。例如,您可以编写my-defun 来检查其主体中的declare-local 并对其进行转换。 更新,但它有点反 Lisp。 Lisp 形式通常只影响嵌套形式。 cl:declare 是我能想到的唯一例外。

【讨论】:

我知道let*,但正如你所说,这不是我正在搜索的内容。我还考虑定义defunlambda 的宏替代方案,它们将搜索declare_local 的出现,通过调用setf 替换它们,并将整个主体包裹在let* 中。这是我能想到的最好的解决方案,尽管它有一个很大的缺陷,即您不能在扩展为原版 defunlambda 的宏中使用 declare_local 因此,declare-local 比 `let* 有更多的缺点,不是吗? :) 好吧,对我来说最好的办法是尽可能使用declare_local,否则使用letlet*。但问题的重点是找到一种万无一失的方法来定义declare_local【参考方案2】:

是的,在 Common Lisp 中使用 &aux 参数:

(defun foo (a b &aux x y z)
  (setq x ...)
  (setq y ...)
  .... )

或使用"prog feature":

(defun bar (a b)
  (prog (x y)
    (setq x a y b) 
    ....
    (return 42)
    ))

【讨论】:

但这意味着我必须提前声明所有变量,就像过去在 C 中所做的那样。此外,您的建议基本上等同于(let (x y) ...)(我真的不需要prog 提供的returngo)。 @Norswap 这就是你想要的,不是吗? - 好吧,所以 prog 并没有真正添加太多,它只是“不让”。 -- 你也可以使用 let 只预先声明一些变量,以获得更接近使用站点的声明和更小的 let 总数(比完全“嵌套”的编码风格)。或者,您可以通过预先完成的所有声明完全消除 let。找到适合您的平衡点。 :) @Norswap 如果你希望你的声明是本地的,而不是嵌套的,那么它必须是一个宏,比如def,它可以识别,比如,(deflocal ...) 表单并将事物重新组合到嵌套中让。 可能没有必要 setq &aux 本地人。试试这个:(funcall (lambda (x &aux (y x)) (list x y)) 1). @Kaz 也许是,但如果表达式很大且笨拙,那么使用 setq 的代码可能更容易阅读。【参考方案3】:

不,没有任何形式可以出现在类似progn 的主体中并突然声明具有progn 其余部分的范围的新变量。

虽然这在某些方面会很方便(例如在版本控制中减少缩进和更少的空白差异),但也有一个很大的缺点:编写分析代码的代码要困难得多。

Lisp 的结构使得当我们查看复合形式的最左边的符号时,我们就知道它是什么,并且在该符号旁边有一些严格的、易于解析的语法,它告诉我们什么符号(如果有的话)是什么在该构造的范围内引入。 (如果它做了这样的事情,它被称为绑定构造)。

可以在整个主体中散布变量定义的绑定构造需要额外的工作才能找到所有这些位置。

如果您真的错过了其他语言的此功能,您可以自己编写一个宏来为您实现它。

这是一个可能的开始。

让我们调用宏(begin ...),在(begin ...) 内部让我们支持(new (var [initform])*) 形式的语法,它使用let 语法引入一个或多个变量,除了它没有主体。这些变量的范围是begin 表单的其余部分。

因此,任务是制作这种形式的宏转换语法:

(begin
  a b c
  (new (x 42))
  d e
  (new (y 'foo) (z))
  f g)

进入,比如说,这段代码:

(progn
  a b c
  (let ((x 42))
    d e
    (let ((y 'foo) (z))
      f g)))

begin 宏必须查看它的所有参数形式,并将 (new ...) 与其他任何形式区分开来,并生成嵌套的 let 结构。

上述转换问题的结构暗示了一个简单的递归解决方案。

现代工业实力begin 必须提供声明。也许我们可以允许(new ...) 表单紧跟(declare ...) 表单。然后根据((new A ...) (declare B ...) C ...) -> (let (A ...) (declare B ...) C ...) 的模式将两者折叠起来,在这里我们递归处理(C ...) 以获得更多的(new ...)

当然,如果你有这个begin 宏,你必须明确地使用它。没有任何简单的方法可以重新定位具有“隐式预测”的现有 Lisp 结构,从而使其具有“隐式开始”。

当然,你总能做的是实现一个非常复杂的宏,如下所示:

(my-dialect-of-lisp
  ;; file full of code in your customized dialect of Lisp goes here
  )

my-dialect-of-lisp 宏解析方言(即为该方言实现完整的代码遍历器)并将翻译翻译成标准 Lisp。


附录:(begin ...)的实现

(不支持声明):

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defun begin-expander (forms)
    (if (null forms)
      nil
      (destructuring-bind (first &rest rest) forms
        (if (and (consp first)
                 (eq (first first) 'new))
          `((let (,@(rest first)) ,@(begin-expander rest)))
          `(,first ,@(begin-expander rest)))))))

(defmacro begin (&rest forms)
  (let ((expansion (begin-expander forms)))
    (cond
      ;; (begin) -> nil
      ((null expansion) nil)
      ;; (begin (new ...) ...) -> ((let (...) ...)) -> (let (...) ...)
      ((and (consp (first expansion))
            (eq (first (first expansion)) 'let))
        (first expansion))
      ;; (begin ...) -> (...) -> (progn ...)
      (t `(progn ,@expansion)))))

这种东西最好借助模式匹配库来表达。

【讨论】:

【参考方案4】:

这是一个概念验证宏,它将变量声明从平面列表中提取到标准 let* 表单中。

(defun my/vardecl-p (x)
  "Return true if X is a (VAR NAME VALUE) form."
  (and (listp x)
       (> (length x) 1)
       (eq 'var (car x))))

(defmacro my/defun (name args &rest body)
  "Special form of DEFUN with a flatter format for LET vars"
  (let ((vardecls (mapcar #'cdr
                          (remove-if-not #'my/vardecl-p body)))
        (realbody (remove-if #'my/vardecl-p body)))
    `(defun ,name ,args
       (let* ,vardecls
         ,@realbody))))

例子:

(my/defun foo (a b)
  (var x 2)
  (var y 3)
  (* x y a b))

(foo 4 5)
; => 120

【讨论】:

目前我看到的最好的解决方案。为了完整,它应该处理嵌套列表并用setf 调用替换声明。就像我在其他地方所说的那样,它的缺点是它不适用于扩展为 defunlambda 的宏。 我不能说我特别喜欢这种 lisp 应该工作的解决方法,但 +1 是为了实际回答这个问题。谁知道?这可能有一天会派上用场。

以上是关于有没有办法在 Lisp 中声明局部变量(避免 let)?的主要内容,如果未能解决你的问题,请参考以下文章

03: JavaScript

JS局部变量和全局变量·你不知道的事

Common Lisp宏变量扩展

JS内存泄露

什么是闭包

关于全局变量和局部变量的理解