如何格式化Lisp代码?

4
考虑一下来自SICP的迭代阶乘过程。
(define (fact-iter product counter max-count)
  (if (> counter max-count)
      product
      (fact-iter (* counter product)
                 (+ counter 1)
                 max-count)))

在这里,我们看到:

  • 阶乘的声明没有前导空格,我认为这很正常。
  • 函数体的每一行都需要两个前导空格。
  • 我们在if语句的第一个子句和第二个子句开头添加了四个空格。总共6个空格。
  • 在最后两行中,即if语句的第二个子句的其余部分中,我们添加了11个额外的空格。总共17个空格。

为什么是这样?这个间距对我来说很困惑。为什么不能像java那样(在代码的每个内部部分添加四个空格)?我应该如何格式化lisp代码?


函数调用的参数与第一个参数对齐。这使得清楚地知道它们是谁的参数,因此您不需要开始计算括号以确定 MAX-COUNTFACT-ITER 的第三个参数(即使 IF 当然不是函数,但同样适用)。它还区分了参数列表和宏体。 - jkiiski
@jkiiski 我们在 "product" 后面添加了 4 个空格以使其与 (if ? 对齐。为什么只在 " (if " 后面添加了两个空格?为什么是 2 个? - lightning_missile
@morbidCode:某些“特殊”的形式,例如定义和绑定形式,有它们自己的规则,我认为(我相信编辑器知道)这些形式的“主体”会缩进两个字符。这包括Scheme中的define,CL中的defun以及任何一种语言中的letlambda。这些通常是概念上存在“主体”的形式。(请注意,我在这里使用“特殊”一词并非技术意义上的)。 - user5920214
2个回答

11

简短概述:它使表达式的嵌套更加清晰


Lisp语言,包括Scheme在内,最具特色的功能之一就是你所编写的代码本质上就是抽象语法树(AST)。像Java这样的语言有很多语法,因此需要解析器来正确消除潜在的歧义语法。中缀运算符就是一个典型的例子。考虑在Java中以下语句:

int x = 1 + y * 2;

代码的文本表示并不意味着任何类似树形结构的东西,但实际上,该语句有一个单一的标准解析结果,确实是一个树形结构。它可能看起来像这样:

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

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

(define x (+ 1 (* y 2)))
注意显式分组如何创建一个非常明确定义的表达式树。不需要像大多数其他语言那样使用运算符优先级规则。这种简单性是一种有意的设计选择,因为当源代码表示如此简单时,编写转换宏非常容易。Lisp通常会广泛使用宏,因为相对于其他编程语言,操作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风格,但会浪费垂直空间,并且由于缩进较少,使AST有点难以立即理解。


如果在Scheme中使用“简单”缩进模型将导致灾难,请考虑以下两个等价表达式:

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

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

第一个例子维护了AST。很容易看出format调用是“else”情况的一部分,因为它嵌套在其中。第二个例子没有保留AST,乍一看不清楚对format的调用是嵌套在if中还是只是作为string->number的第二个参数传递。你可以看到,Lisp的语法并没有真正地表明这一点。

Scheme缩进可能一开始看起来有点奇怪,但一旦你习惯了它,就不必在脑海中处理括号,代码就更容易理解了。语法的统一既是一种福音,又是一种诅咒:它使编写宏变得微不足道,但也移除了一些让代码更容易理解的视觉标记。有一个更语义化的缩进系统可以帮助缓解这个缺点。


1
有没有自动执行这个操作的工具? - lightning_missile
2
@morbidCode 是的,但这取决于您使用的编辑器。DrRacket有自动缩进功能。Emacs有不同的主要模式,可用于编辑各种lisp代码。其他编辑器也具有类似的功能。 - Alexis King

7

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

  • 宏和特殊形式可以有自定义的缩进规则。请参见下文。

  • 根据可用的水平空间,函数有几个缩进变体

例子:

(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这样简单的宏/运算符通常与函数对齐。

稍微复杂一些的情况是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美化打印机将根据可用的水平空间选择缩进变体。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接