【问题标题】:How to format lisp code?如何格式化lisp代码?
【发布时间】:2016-07-04 22:39:34
【问题描述】:

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

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

在这里,我们看到了:

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

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

【问题讨论】:

  • 函数调用的参数与第一个一致。这清楚地表明它们是谁的论点,所以你不需要开始计算括号来确定MAX-COUNTFACT-ITER 的第三个参数(同样适用于IF,尽管它当然不是一个函数)。它还将参数列表与宏体区分开来。
  • @jkiiski 我们在 "product" 中添加了 4 个空格以使其与 (if ? 对吗?为什么只在 " (if " 中添加两个空格?为什么是 2 个?
  • @morbidCode:某些“特殊”表单,例如定义表单和绑定表单,有自己的规则,即(我认为:我相信编辑知道)表单的“正文”会缩进由两个字符。这包括诸如 Scheme 中的 define、CL 中的 defun 以及两者中的 letlambda 之类的东西。这些通常是“身体”概念有意义的形式。 (请注意,我在这里使用“特殊”是非技术意义上的)。

标签: scheme lisp code-formatting


【解决方案1】:

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


Lisps 的定义特性之一,包括 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 调用是if 表单的“else”案例,因为它嵌套在它之下。第二个示例不维护 AST,乍一看并不清楚对 format 的调用是否嵌套在 if 内,或者它是否只是传递给 string->number 的第二个参数。您可以看到 Lisp 的语法并没有真正说明这一点。

方案缩进起初看起来有点古怪,但是一旦你习惯了它,它会让代码更容易看到,而不必在脑海中摆弄括号。语法的统一性既是福也是祸:它使编写宏变得微不足道,但它删除了一些使代码更容易理解的视觉标记。拥有更语义化的缩进系统有助于缓解这个缺点。

【讨论】:

  • 是否有自动执行此操作的工具?
  • @morbidCode 是的,但这取决于您的编辑器。 DrRacket 具有自动缩进功能。 Emacs 有各种主要的模式来编辑不同类型的 lisp 代码。其他编辑器也有类似的功能。
【解决方案2】:

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 漂亮打印机会根据可用的水平空间选择缩进变体。

【讨论】:

    猜你喜欢
    • 2010-10-07
    • 1970-01-01
    • 2011-04-13
    • 2013-09-09
    • 1970-01-01
    • 2022-01-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多