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))))
通常尾递归会被编译器优化以减少栈调用, 从而效率更高. 但是两种形式都是比较好的代码风格.