Lisp Style Guide
About
以下是一篇在 CMU Lisp Style Guide 中的 Lisp 风格指南的简单翻译和笔记.
(注: 应该是部分的翻译. )
How to be a Stylish Lisp Programmer
General Programming Style
- 短的函数 (易读, 易写, 易调试, 易懂)
- 说明性的变量/函数名称
Lisp Specific Guidelines
- 别像 Pascal 或者 C 代码一样写 Lisp
- 尽量用已有的函数 – 参考 CLtL2 或者使用
apropos
或者describe
. - 不要让括号单独成行 – 代码结构应当通过缩进表现. 大多数 Lisp 系的编辑器都包含了自动括号配对, 括号间跳转, 括号操作, 自动代码缩进的功能.
- 尽量用已有的函数 – 参考 CLtL2 或者使用
- 如果刚接触 Lisp, 请避免使用下面的函数/宏/特殊形式 – 大部分时间你没必要用它们:
eval
,progv
,catch
,throw
,block
, 以及return-from
. 类似的, 尽量避免破坏性 (带副作用的) 的函数, 如nconc
,sort
,delete
,rplaca
, 以及replacd
. 因为它们可能不会如你所愿地工作. 如果你一定要用rplaca
和rplacd
, 请使用(setf (car ...) ..)
以及(setf (cdr ..) ..)
来代替. - 避免使用
prog
,go
以及tagbody
. 请直接使用循环函数或者写一个递归函数来代替. - 不要用类似
c{a,d}r
这样但是c
,r
中间a
,d
数量超过两个即以上的函数 (如cddr
,cadar
等), 因为它们真的很难读. 请使用first
,second
,third
,nth
以及nthcdr
来代替. - 使用
cond
来代替if
和progn
的组合. 使用cond
而不是嵌套的if
. 请记住要检查并排除不可能发生cond
条件分支. - 不要使用单条件分支或者某一个条件分支返回值为
nil
的if
. 使用when
或者unless
来代替. - 尽可能使用局部变量而不是全局变量. 用
let
或者let*
来定义局部变量. 如果一定要使用全局变量, 用*
包围名字的方式来对全局变量进行命名, 这有助于其在代码中容易被看出来. - 当
nil
被用于做空列表时, 请使用()
做字面量. 当nil
被用于布尔运算时, 请使用nil
做字面量. 类似的, 使用null
来判断空列表;not
来判断逻辑值. 使用endp
来判断是否是列表的尾部, 而不是null
. - 当能够用
dotimes
或者dolist
时, 不要用do
. 能用let
时不要用let*
. 能用flet
时不要用labels
. 能用do
时不要用do*
. - 不要在
case
判断中使用quote
. 如(case x ('a 3) ..)
, 这时不对的, 因为这会等价于判断x
是否是 ='(quote a)=. 使用(case x (a 3))
来代替. - 避免用
apply
来将列表进行扁平化. 函数可能会接受有限个参数, 而构造类似于(apply #'append list-of-lists)
可能会产生冲突. - 不要在你之后想要按结构修改的地方使用
quote
. 比如:(defun foo () (let ((var '(c d))) ..))
不要用 ='(c d)=, 而是用
(list 'c 'd)
. - 不要混淆
listp
和consp
.consp
将会判断是否是 cons 的结构, 即对nil
会返回nil
, 而listp
更类似于:(defun listp (x) (or (null x) (consp x)))
- 当对两个数做除法的时候, 如果想要得到一个实数的话, 记得使用
tuncate
和round
, 否则可能会得到一个分数.
Documentation
- 在 top-level expression 的前后至少放一行空行.
- 对于左对齐的注释, 使用
;;;
三个分号开头. 你可以在这里描述一些类似于代码功能, 使用的算法, 以及其他的解释性的文字. - 在函数定义的时候带上 documentation string. 有助于帮助用户了解函数功能.
- 对于和代码平齐浮动的注释, 使用
;;
两个分号开头. 大多数 Lisp 系的编辑器可以根据代码自动缩进这些注释. - 对于代码右侧的短注释, 使用
;
一个分号开头. - 感性地认识就是, 使用的分号的大概和注释的长度差不多.
- 如果你需要注释一小段代码, 使用左对齐的单个分号.
Examples
Documentation
;;; A function to compute the nth fibonacci number.
(defun fib (n)
"Returns the nth Fibonacci number, where f(n) = f(n-1) + f(n-2); f(1)=f(0)=1."
;; check for the two base cases, otherwise recurse
(cond ((zerop n) 1) ; zerop is potentially faster than (= n 0)
((= n 1) 1)
(t (+ (fib (1- n))
(fib (- n 2))))))
Control Structures
Poor style:
(if <condition>
(progn <statement1>
<statement2>
<statement3>))
Good style:
(when <condition>
<statement1>
<statement2>
<statement3>)
Poor style:
(if <condition>
nil
(progn <statement1>
<statement2>
<statement3>))
Good style:
(unless <condition>
<statement1>
<statement2>
<statement3>)
Poor style:
(if <condition>
(progn <statement1>
<statement2>)
(progn <statement3>
<statement4>))
Good style:
(cond (<condition>
<statement1> <statement2>)
(t <statement3> <statement4>))
Poor style:
(if <condition1>
<statement1>
(if <condition2>
<statement2>
(if <condition3>
<statement3>)))
Good style:
(cond (<condition1> <statement1>)
(<condition2> <statement2>)
(<condition3> <statement3>))
Poor style:
(cond (<condition> t)
(t <statement>))
Good style:
(if <condition>
t
<statement>)
Poor style:
(defun function (arg1 arg2)
(setf local1 value1)
(setf local2 value2)
<statement>)
Good style:
(defun function (arg1 arg2)
(let ((local1 value1)
(local2 value2))
<statement>))
在上面的 setf
的例子里面, local1
和 local2
实际上是在函数外也是可见的全局变量.
而 let
构造的局部变量仅仅在 let
声明的环境中可见.
Iteration
递归的一个模版
(defun function (arg1 arg2)
(if end-condition
return-result
recurse))
例:
(defun factorial (n)
(if (zerop n)
1
(* n (factorial (1- n)))))
;; 尾递归 tail recursion 形式:
(defun factorial (n &optional (result 1))
(if (zerop n)
result
(factorial (1- n) (* n result))))
通常尾递归会被编译器优化以减少栈调用, 从而效率更高. 但是两种形式都是比较好的代码风格.