优秀的Lisp编程风格教程:第一章(译文)

原文链接:https://norvig.com/luv-slides.ps

1. 什么是优秀的风格?

优秀的Lisp编程风格

“优雅不是可选的”(elegance is not optional)–Richard A. O’Keefe

(在任何语言中)优秀的风格使得程序:

  • 可以理解的
  • 可重复使用的
  • 可扩展的
  • 高效的
  • 易于开发/调试

它还有助于正确性、健壮性和兼容性

我们的优秀风格的准则是:

  • 明确的
  • 具体的
  • 简洁的
  • 一致的
  • 有帮助的(预见读者的需求)
  • 保持惯例(不要晦涩)
  • 在可用的层级上构建抽象
  • 允许工具交互(引用透明性)

好的风格是支撑程序的“底层”

1.1 好的风格从何而来?

该相信什么

不要相信我们告诉你的一切。(大多数)

少担心该相信什么,多担心为什么。知道你的“风格规则”从何而来:

  • 宗教,善与恶 “这样更好。”
  • 哲学 “这与其他事物是一致的。”
  • 稳健性、责任、安全、道德 “我会进行冗余检查,以避免发生可怕的事情。”
  • 合法性 “我们的律师说要这样做。”
  • 个性,观点 “我喜欢这样。”
  • 兼容性 “另一个工具期望这种方式。”
  • 可移植性 “其他编译器更喜欢这种方式。”
  • 合作,公约“必须采用统一的方式,所以我们就这个达成了一致。”
  • 习惯,传统 “我们一直是这样做的。”
  • 能力 “我的程序员不够老练。”
  • 记忆 “知道我要怎么做这件事意味着我不需要记住我是怎么做的。”
  • 迷信 “我害怕做不一样的事。”
  • 实用性 “这让其他事情变得更容易。”
这一切都是关于沟通

表达 + 理解 = 沟通

程序和以下对象沟通:

  • 人类读者
  • 编译器
  • 文本编辑器(arglist,doc string,indent)
  • 工具(trace, step, apropos, xref, manual)
  • 程序的用户(间接沟通)
了解上下文

阅读代码的时候:

  • 知道谁写的以及什么时候写的

编写代码的时候:

  • 用注释作说明
  • 在你的注释上签名并注明日期!(应该是一个编辑器命令来做到这一点)

需要注意的一些事情:

  • 人们的风格随着时间而变化。
  • 同一个人在不同的时间看起来像是不同的人。
  • 有时候那个人就是你自己。

1.2 我怎么知道它好不好?

价值体系不是绝对的
不能孤立地查看风格规则。它们经常以相互矛盾的方式重叠。

风格规则相互冲突的事实反映了现实世界目标相互冲突的自然事实。优秀的程序员会在编程风格上做出权衡,以反映以下各种主要目标之间的潜在优先级选择:

  • 可以理解的
  • 可重复使用的
  • 可扩展的
  • 高效的(编码,空间,速度…)
  • 易于开发/调试
为什么好的风格是好的

良好的风格有助于构建当前程序和下一个程序:

  • 组织程序,缓解人类记忆需求
  • 鼓励模块化、可重用的部件

风格不是最后才添加的。它作用于:

  • 将程序组织成文件
  • 顶层设计,每个文件的结构和布局
  • 分解为模块和组件
  • 数据结构的选择
  • 单个功能的设计/实现
  • 命名、格式和文档标准
为什么风格是实用的:记忆

“当我年轻的时候,我能想象一座有20个房间的城堡,每个房间里有10件不同的物品。我没有问题。我不能再这样了。现在我更多地考虑早期的经历。我看到的是一团初生的云,而不是明信片上的清晰。但我确实能写出更好的程序。” - Charles Simonyi

“有些人是优秀的程序员,因为他们可以处理比大多数人更多的细节。但根据这个原因来选择程序员有很多缺点——这可能会导致其他人无法维护的程序。”- Butler Lampson

“在我的程序里随便挑三行,我就能告诉你它们来自哪里,做什么。”- David McDonald

好的风格取代了对大量记忆的需求:

  • 确保任何3 (5? 10?) 行是自解释的
    也称为“引用透明”
    将复杂性打包成对象和抽象;而不是全局变量/依赖项
  • 使其向上/向下“分形”自组织
  • 表达你的真实想法(Say what you mean)
  • 说话算话(Mean what you say)
为什么风格是实用的:复用

结构化编程鼓励满足规范并能在规范范围内重用的模块。

分层设计鼓励使用具有通用功能的模块,即使规范发生变化,也可以在另一个程序中重用这些功能。

面向对象设计是一种侧重于对象类和信息隐藏的分层设计。

您应该以重用为目标:

  • 数据类型(类)
  • 函数(方法)
  • 控制抽象
  • 接口抽象(包,模块)
  • 语义抽象(宏和整个语言)
表达你的真实想法(Say what you mean)

“简单而直接地说出你的意思”(Say what you mean, simply and directly) - Kernighan & Plauger

在数据中表达你的真实想法(具体的,简洁的):

  • 使用数据抽象
  • 为数据定义新的语言(如果需要的话)
  • 明智地取名字

在代码中表达你的真实想法(简洁的,按照惯例):

  • 明确地定义接口
  • 适当地使用宏和语言
  • 使用内置函数
  • 创建你自己的抽象
  • 能做一次就不要做第二次

在注释中(明确的,有用的):

  • 在注释中使用适当的细节
  • 文档字符串比注释更好
  • 说说它的用途,而不仅仅是它做了什么
  • 声明和断言
  • 系统 (以及测试文件, 等等.)
显式的
可选参数和关键字参数

如果必须查找默认值,则需要提供它。只有当你真的相信你不在乎或者你确信默认值被所有人理解和接受时,你才应该接受默认值。

例如,当打开一个文件时,你几乎不应该考虑省略:direction关键字参数,即便你知道它默认为:input

声明

如果知道类型信息,就声明它。不要像某些人那样,只声明你知道编译器会使用的东西。编译器会改变,您希望您的程序自然地利用这些变化,而不需要持续的干预。

此外,声明也是为了与人类读者交流 - 而不仅仅是编译器。

注释

如果您想到了一些有用的东西,而其他人在阅读您的代码时可能想知道这些东西,而这些东西对他们来说可能不会立即显现出来,那么请将其写为注释。

具体的

只要您的数据抽象有必要,就尽量具体,但不要过多。

选择:

;; 更具体
(mapc #'process-word
    (first sentences))

;; 更抽象
(map nil #'process-word
    (elt sentences 0))

最具体的条件:

  • if 用于两分支表达式
  • whenunless 用于单分支语句
  • andor仅用于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)) )) ))
  • 两次返回0
  • 非标准缩进
  • 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?为什么不是lengthmapcar
  • 为什么不用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

一致的

有些操作符对具有重叠的功能。在中性的情况下(任何一种都可以使用时)使用哪一种要保持一致,这样当你在做一些不寻常的事情时就很明显了。

下面是letlet*的例子。第一个使用并行绑定,第二个使用顺序绑定。第三种是中性的。

(let ((a b) (b a)) ...)

(let* ((a b) (b (* 2 a)) (c (+ b 1))) ...)

(let ((a (* x (+ 2 y))) (b (* y (+ 2 x)))) ...)

下面是类似的使用fletlabels的示例。第一种方法利用了局部函数的闭包,第二种方法利用了非闭包。第三种是中性的。

(labels ((process (x) ... (process (cdr x)) ...)) ...)

(flet ((foo (x) (+ (foo x) 1))) ...)

(flet ((add3 (x) (+ x 3))) ...)

在两个示例中,你都可以反过来选择,在中性的情况下总是选择let*labels,以及不寻常的情况下总是选择letflet。一致性比实际的选择更重要。然而,大部分人认为letflet是正常的选择。

选择合适的语言

选择合适的语言,并在选择的语言中使用合适的功能。Lisp并不是适用于所有问题的语言。

“你得跟那个带你来的人跳舞”-- Bear Bryant

Lisp的好处是:

  • 探索性程序设计
  • 快速原型
  • 减少投放市场的时间
  • 单程序员(或单位数团队)项目
  • 源到源或数据到数据转换
    编译器和其他翻译器
    问题特定的语言
  • 动态分派和创建(运行时可用编译器)
  • 在一个内存映像中紧密集成模块(与Unix的字符管道模型相反)
  • 高度交互(读取-执行-打印,CLIM)
  • 用户可扩展的应用(GNU Emacs)

“我相信好的软件是由两个、三个或四个人组成的小团队在非常高、密集的水平上相互交流编写的。” - John Warnock

“一旦你成为一名有经验的Lisp程序员,你就很难再回到其他语言了。” - Robert R. Kessler

目前的Lisp实现不太适合:

  • 持久化存储(数据库)
  • 在小型机器上最大化地利用资源
  • 拥有数百名程序员的项目
  • 与外部代码交互
  • 交付小内存映像的应用程序
  • 实时控制(但Gensym做到了)
  • 没有经验的Lisp程序员的项目
  • 某些类型的数值或字符计算(需要仔细的声明,但Lisp效率模型很难学习)。

猜你喜欢

转载自blog.csdn.net/zssrxt/article/details/132924560