原文链接:https://norvig.com/luv-slides.ps
“优雅不是可选的”(elegance is not optional)–Richard A. O’Keefe
(在任何语言中)优秀的风格使得程序:
它还有助于正确性、健壮性和兼容性
我们的优秀风格的准则是:
好的风格是支撑程序的“底层”
不要相信我们告诉你的一切。(大多数)
少担心该相信什么,多担心为什么。知道你的“风格规则”从何而来:
表达 + 理解 = 沟通
程序和以下对象沟通:
阅读代码的时候:
编写代码的时候:
需要注意的一些事情:
不能孤立地查看风格规则。它们经常以相互矛盾的方式重叠。
风格规则相互冲突的事实反映了现实世界目标相互冲突的自然事实。优秀的程序员会在编程风格上做出权衡,以反映以下各种主要目标之间的潜在优先级选择:
良好的风格有助于构建当前程序和下一个程序:
风格不是最后才添加的。它作用于:
“当我年轻的时候,我能想象一座有20个房间的城堡,每个房间里有10件不同的物品。我没有问题。我不能再这样了。现在我更多地考虑早期的经历。我看到的是一团初生的云,而不是明信片上的清晰。但我确实能写出更好的程序。” - Charles Simonyi
“有些人是优秀的程序员,因为他们可以处理比大多数人更多的细节。但根据这个原因来选择程序员有很多缺点——这可能会导致其他人无法维护的程序。”- Butler Lampson
“在我的程序里随便挑三行,我就能告诉你它们来自哪里,做什么。”- David McDonald
好的风格取代了对大量记忆的需求:
结构化编程鼓励满足规范并能在规范范围内重用的模块。
分层设计鼓励使用具有通用功能的模块,即使规范发生变化,也可以在另一个程序中重用这些功能。
面向对象设计是一种侧重于对象类和信息隐藏的分层设计。
您应该以重用为目标:
“简单而直接地说出你的意思”(Say what you mean, simply and directly) - Kernighan & Plauger
在数据中表达你的真实想法(具体的,简洁的):
在代码中表达你的真实想法(简洁的,按照惯例):
在注释中(明确的,有用的):
如果必须查找默认值,则需要提供它。只有当你真的相信你不在乎或者你确信默认值被所有人理解和接受时,你才应该接受默认值。
例如,当打开一个文件时,你几乎不应该考虑省略:direction
关键字参数,即便你知道它默认为:input
。
如果知道类型信息,就声明它。不要像某些人那样,只声明你知道编译器会使用的东西。编译器会改变,您希望您的程序自然地利用这些变化,而不需要持续的干预。
此外,声明也是为了与人类读者交流 - 而不仅仅是编译器。
如果您想到了一些有用的东西,而其他人在阅读您的代码时可能想知道这些东西,而这些东西对他们来说可能不会立即显现出来,那么请将其写为注释。
只要您的数据抽象有必要,就尽量具体,但不要过多。
选择:
;; 更具体
(mapc #'process-word
(first sentences))
;; 更抽象
(map nil #'process-word
(elt sentences 0))
最具体的条件:
if
用于两分支表达式when
、unless
用于单分支语句and
、or
仅用于boolean值cond
用于多分支语句或表达式;; 违反期望:
(and (numberp x) (cos x))
(if (numberp x) (cos x))
(if (numberp x) (print x))
;;遵循期望:
(and (numberp x) (> x 3))
(if (numberp x) (cos x) nil)
(when (numberp x) (print x))
测试最简单的情况。如果在两个地方进行相同的测试(或返回相同的结果),一定有更简单的方法。
差的: 冗长的,复杂的
(defun count-all-numbers (alist)
(cond
((null alist) 0)
(t (+ (if (listp (first alist))
(count-all-numbers (first alist))
(if (numberp (first alist)) 1 0))
(count-all-numbers (rest alist)) )) ))
alist
建议用于关联列表好的:
(defun count-all-numbers (exp)
(typecase exp
(cons (+ (count-all-numbers (first exp))
(count-all-numbers (rest exp))))
(number 1)
(t 0)))
cond
而不是typecase
同样好(但不那么具体,更常规,更一致)。
最大化 LOCNW(LOCNW:没有编写的代码行,意为尽可能减少代码行)
“Shorter is better and shortest is best.”- Jim Meehan
差的: 太啰嗦,效率低下
(defun vector-add (x y)
(let ((z nil) n)
(setq n (min (list-length x) (list-length y)))
(dotimes (j n (reverse z))
(setq z (cons (+ (nth j x) (nth j y)) z)))))
(defun matrix-add (A B)
(let ((C nil) m)
(setq m (min (list-length A) (list-length B)))
(dotimes (i m (reverse C))
(setq C (cons (vector-add (nth i A)
(nth i B)) C)))))
nth
的使用使得复杂度变为O(n^2)list-length
?为什么不是length
或mapcar
?nreverse
更好的: 更简洁
(defun vector-add (x y)
"Element-wise add of two vectors"
(mapcar #'+ x y))
(defun matrix-add (A B)
"Element-wise add of two matrices (lists of lists)"
(mapcar #'vector-add A B))
或者使用广义函数:
(defun add (&rest args)
"Generic addition"
(if (null args)
0
(reduce #'binary-add args)))
(defmethod binary-add ((x number) (y number))
(+ x y))
(defmethod binary-add ((x sequence) (y sequence))
(map (type-of x) #'binary-add x y))
文档应该围绕用户需要完成的任务来组织,而不是围绕你的程序碰巧提供了什么。向每个函数添加文档字符串通常不会告诉读者如何使用您的程序,但是在适当的地方提供提示可能非常有效。
好的: (来自GNU Emacs在线帮助)
next-line: Move cursor vertically down ARG lines.
...If you are thinking of using this in a Lisp program,
consider using `forward-line' instead. It is usually eas-
ier to use and more reliable (no dependence on goal
column, etc.).
defun: defines NAME as a function. The definition
is (lambda ARGLIST [DOCSTRING] BODY...). See also the
function interactive.
这些预测用户的使用和问题。
构建自己的功能以与现有功能并行
遵守命名约定:
with-something, do something 宏
可能的话使用内置的功能
差的: 非惯例的
(defun add-to-list (elt list)
(cond ((member elt lst) lst)
(t (cons elt lst))))
好的: 使用内置函数
(作为练习)
“使用库函数”- Kernighan & Plauger
有些操作符对具有重叠的功能。在中性的情况下(任何一种都可以使用时)使用哪一种要保持一致,这样当你在做一些不寻常的事情时就很明显了。
下面是let
和let*
的例子。第一个使用并行绑定,第二个使用顺序绑定。第三种是中性的。
(let ((a b) (b a)) ...)
(let* ((a b) (b (* 2 a)) (c (+ b 1))) ...)
(let ((a (* x (+ 2 y))) (b (* y (+ 2 x)))) ...)
下面是类似的使用flet
和labels
的示例。第一种方法利用了局部函数的闭包,第二种方法利用了非闭包。第三种是中性的。
(labels ((process (x) ... (process (cdr x)) ...)) ...)
(flet ((foo (x) (+ (foo x) 1))) ...)
(flet ((add3 (x) (+ x 3))) ...)
在两个示例中,你都可以反过来选择,在中性的情况下总是选择let*
或labels
,以及不寻常的情况下总是选择let
或flet
。一致性比实际的选择更重要。然而,大部分人认为let
和flet
是正常的选择。
选择合适的语言,并在选择的语言中使用合适的功能。Lisp并不是适用于所有问题的语言。
“你得跟那个带你来的人跳舞”-- Bear Bryant
Lisp的好处是:
“我相信好的软件是由两个、三个或四个人组成的小团队在非常高、密集的水平上相互交流编写的。” - John Warnock
“一旦你成为一名有经验的Lisp程序员,你就很难再回到其他语言了。” - Robert R. Kessler
目前的Lisp实现不太适合: