如何格式化 lisp 代码?

How to format lisp code?

考虑 SICP 中的这个迭代阶乘过程。

(define (fact-iter product counter max-count)
  (if (> counter max-count)
      product
      (fact-iter (* counter product)
                 (+ counter 1)
                 max-count)))

在这里,我们看到:

为什么会这样?间距让我感到困惑。为什么不能像java(在代码的每个内部部分添加四个空格)?我应该如何格式化 lisp 代码?

tl;dr:它使表达式的嵌套变得清晰


Lisps(包括 Scheme)的一个定义特征是您编写的代码本质上是 AST(抽象语法树)。像 Java 这样的语言有很多语法,所以它们需要解析器来正确地消除潜在歧义的语法。中缀运算符就是一个典型的例子。考虑 Java 中的以下语句:

int x = 1 + y * 2;

代码的文本表示当然并不意味着任何树状结构,但实际上,该语句有一个单一的规范解析,实际上是一棵树。它看起来像这样:

     =
    / \
int x  +
      / \
     1   *
        / \
       y   2

另一方面,等效的 Scheme 代码使所有嵌套变得非常明确:

(define x (+ 1 (* y 2)))

注意显式分组是如何创建定义非常明确的表达式树的。不需要像大多数其他语言那样的运算符优先级规则。这种简单性是一种有意的设计选择,因为当源代码表示如此简单时,编写对其进行转换的宏非常容易。 Lisps 倾向于大量使用宏,因为由于语法简单,与其他编程语言相比,操作 AST 相对轻松。


考虑到所有这些,缩进规则可能会变得更加明显:Lisp 代码通常以这样的方式缩进,以便 AST 的结构立即可见。考虑使用“更简单”缩进样式的 fact-iter 示例函数的替代版本:

(define (fact-iter product counter max-count)
  (if (> counter max-count)
    product
    (fact-iter (* counter product)
      (+ counter 1)
      max-count)))

在这种特殊情况下,缩进不是灾难性的,但对 fact-iter 的递归调用现在更难以直观地解析。 Lisp/Scheme 语法的统一性使得很难立即发现 fact-iter 是用三个参数调用的事实,因为第一个参数不再与最后两个参数对齐。

这至少可以通过将所有参数放在不同的行中来解决:

(fact-iter
 (* counter product)
 (+ counter 1)
 max-count)

这行得通,实际上是可以接受的 Lisp 风格。不过,这通常是对垂直 space 的重大浪费,而且它仍然使 AST 更难立即理解,因为缩进在视觉上不那么引人注目。


对于在 Scheme 中使用“更简单”的缩进模型是灾难性的示例,请考虑以下两个等效表达式:

(string->number (if (string? x) x
                    (format "~a" x)))

(string->number (if (string? x) x
  (format "~a" x)))

第一个例子维护了 AST。很容易看出 format 调用是 if 形式的“else”情况,因为它嵌套在它下面。第二个示例不维护 AST,乍一看不清楚 format 的调用是否嵌套在 if 中,或者它是否只是传递给 string->number 的第二个参数。你可以看到 Lisp 的语法并没有真正说明这一点。

Scheme 缩进起初看起来有点古怪,但一旦您习惯了它,它就会使代码更容易看清,而无需在脑海中处理括号。语法的统一既是福也是祸:它使编写宏变得微不足道,但它删除了一些使代码更易于理解的视觉标记。拥有更具语义的缩进系统有助于减轻这一缺点。

Lisp 有一些缩进代码的规则。使用最紧凑的版本。列表中元素的对齐很重要。还要考虑水平 space 与可读性的最佳使用。

  • 宏和特殊表格可以自定义缩进规则。见下文。

  • 函数有一些基于可用水平缩进的变体 space

示例:

(append a b c)    ; all arguments fit on a line

(append a         ; arguments are aligned
        b
        c)

(append           ; saving horizontal space, elements are aligned
 a
 b
 c)

像 IF THEN ELSE 这样的简单 macro/operator 通常像函数一样对齐。

稍微复杂一点的情况是DEFINE:

(define (FUNCTION-NAME ARG0 ... ARGN) BODY-FORM-0 ... BODY-FORM-N)

示例:

 (define (foo a b) (print a) (append a b))

 (define (foo a b)
   (print a)
   (append a b))

 (define (foo a
              b)
   (print a)
   (append a b))

 (define (foo
          a
          b)
   (print a)
   (append a b))

典型的 Lisp 漂亮打印机会根据可用的水平方向选择缩进变体 space。