如何格式化 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)))
在这里,我们看到:
- 阶乘的声明没有前导空格。我觉得这很正常。
- 函数体每行需要两个前导空格。
- 我们在 if 语句的第一个子句和第二个子句的开头添加了四个空格。共6个空格。
- 我们在最后两行添加了 11 个空格,剩下的是 if 语句的第二个子句。共17个空格。
为什么会这样?间距让我感到困惑。为什么不能像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。
考虑 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 代码?
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。