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

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

2. 内置功能

“毫无疑问,Common Lisp是一门庞大的语言”-- Guy Steele

  • 622个内置函数
  • 86个宏
  • 27个特殊表达式形式
  • 54个变量
  • 64个常量

但是什么才算语言本身呢?

  • C++有48个保留字
  • ANSI CL少到只有25个特殊表达式形式
  • 其余部分可以被认为是一个必需的库

不管怎样,Lisp程序员都需要一些帮助:
使用哪些内置功能
如何使用它

DEFVAR 和 DEFPARAMETER

对于不想在重新加载时重新初始化的东西,使用defvar

(defvar *options* '())
(defun add-option (x) (pushnew x *options*))

在重新加载文件之前,您可能已经执行了许多次(add-option…),有些甚至是从另一个文件加载的。通常不希望因为重新加载这个定义而丢掉所有数据。

另一方面,某些类型的选项确实希望在重新加载时重新初始化…

(defparameter *use-experimental-mode* nil
  "Set this to T when experimental code works.")

稍后,您可能会编辑此文件并将变量设置为T,然后重新加载它,希望看到编辑的效果。

建议:忽略CLtL中提到defvar用于变量而defparameter用于参数的部分。它们之间唯一有用的区别是,defvar仅在变量未绑定时才进行赋值,而defparameter无条件地进行赋值。

EVAL-WHEN

(eval-when (:execute) ...)

=

(eval-when (:compile-toplevel) ...)
(eval-when (:load-toplevel) ...)

当然要注意下显式嵌套的eval-when表达式形式,对大多数人来说,这种效果通常不是直观的。

使用FLET来避免代码重复

思考下面示例中对(f (g (h)))的重复使用:

(do ((x (f (g (h)))
        (f (g (h)))))
  (nil) ...)

每次你编辑其中一个(f (g (h))),你可能也需要编辑另一个。这里有一个更好的模块化方式:

(flet ((fgh () (f (g (h)))))
  (do ((x (fgh) (fgh))) (nil) ...))

(这可能被用作do的参数。)

类似地,你可以使用局部函数来避免仅在动态状态上不同的代码分支中的重复。例如,

(defmacro handler-case-if (test form &rest cases)
  (let ((do-it (gensym "DO-IT")))
    `(flet ((,do-it () ,form))
      (if test
          (handler-case (,do-it) ,@cases)
          (,do-it)))))

DEFPACKAGE

大型程序设计由一种设计风格支持,这种风格将代码分成具有明确设计接口的模块。

Common Lisp包系统用来避免模块之间的命名冲突,并且给每个模块定义接口。

  • 这里没有顶层(是线程安全的)
  • 这里有其他程序(使用包)
  • 为你的用户提供方便。只导出用户需要的东西
  • 为维护人员提供方便。更改非导出部分的许可证
(defpackage "PARSER"
  (:use "LISP" #+Lucid "LCL" #+Allegro "EXCL")
  (:export "PARSE" "PARSE-FILE" "START-PARSER-WINDOW"
           "DEFINE-GRAMMAR" "DEFINE-TOKENIZER"))

有些人把导出的符号放在文件顶部定义它们的地方。

我们觉得最好将它们放在defpackage中,并使用编辑器找到相应的定义。

理解状况(Condition)和错误(Error)

Lisp通过提供一个活跃的状况系统来确保代码中的大多数错误不会破坏数据。

了解错误状况之间的区别。
所有的错误都是状况;但并非所有的状况都是错误。

区别三个概念:

  • 发出一个状况 - 发现不寻常的事情发生了。
  • 提供一个重启动 - 从可能的几个选项中选择一个继续。
  • 处理一个状况 - 从可用选项中选择如何继续。

错误检测

选择符合您意图的错误检测和处理级别。通常您不想让坏数据消失,但在许多情况下,您也不想因为无关紧要的原因而进入调试器。

在适合您的应用程序的宽容和挑剔之间取得平衡。

差的: 如果它不是整数怎么办?

(defun parse-date (string)
  "Read a date from a string. ..."
  (multiple-value-bind (day-of-month string-position)
      (parse-integer string :junk-allowed t)
    ...))

可疑的: 如果内存耗尽怎么办?

(ignore-errors (parse-date string))

较好的: 只捕获预期的错误

(handler-case (parse-date string)
  (parse-error nil))

编写优秀的错误信息

  • 在错误信息中使用完整的句子(开头大写,结尾句号)。
  • 没有“Error:”或“;;”前缀。如果需要,系统会提供这样一个前缀。
  • 不要以请求新行开始错误消息。如有必要,系统会自动添加。
  • 与其他格式化字符串一样,不要使用内嵌的制表符。
  • 不要在错误消息中提及后果。只是描述一下情况本身。
  • 不要在描述如何继续时假定调试器的用户界面。 这可能会导致可移植性问题,因为不同的实现使用不同的接口。只需描述给定动作的抽象效果即可。
  • 在消息中指定足够的详细信息,以将其与其他错误区分开来,并且如果可以的话,足以帮助您在以后发生问题时进行调试。

差的:

(error "~%>> Error: Foo. Type :C to continue.")

较好的:

(cerror "Specify a replacement sentence interactively."
  "An ill-formed sentence was encountered:~% ~A"
  sentence)

使用状况系统

从这开始

  • error, cerror
  • warn
  • handler-case
  • with-simple-restart
  • unwind-protect

好的: 警告的标准用法

(defvar *word* '?? "The word we are currently working on.")

(defun lex-warn (format-str &rest args)
  "Lexical warning; like warn, but first tells what word
  caused the warning."
  (warn "For word ~a: ~?" *word* format-str args))

HANDLER-CASE, WITH-SIMPLE-RESTART

好的: 处理具体的错误

(defun eval-exp (exp)
  "If possible evaluate this exp; otherwise return it."
  ;; Guard against errors in evaluating exp
  (handler-case
    (if (and (fboundp (op exp))
             (every #'is-constant (args exp)))
        (eval exp)
        exp)
    (arithmetic-error () exp)))

好的: 提供重启动

(defun top-level (&key (prompt "=> ") (read #'read)
                       (eval #'eval) (print #'print))
  "A read-eval-print loop."
  (with-simple-restart
      (abort "Exit out of the top level.")
    (loop
      (with-simple-restart
          (abort "Return to top level loop.")
        (format t "~&~a" prompt)
        (funcall print (funcall eval (funcall read)))))))

UNWIND-PROTECT

unwind-protect实现了每个人都应该知道如何使用的重要功能。它不仅仅适用于系统程序员。

不过,要注意多任务处理。例如,使用unwind-protect实现某些类型的状态绑定可能在单线程环境中运行良好,但在多任务环境中,您通常必须更加小心。

(unwind-protect (progn form1 form2 ... formn)
  cleanup1 cleanup2 ... cleanupn )
  • 永远不要假设form1会运行。
  • 永远不要假设formn不会运行完成。

通常,您需要在进入unwind-protect之前保存状态,并在恢复状态之前进行测试:

可能不好: (带有多任务)

(catch 'robot-op
  (unwind-protect
    (progn (turn-on-motor)
           (manipulate) )
    (turn-off-motor)))

好的: (更安全)

(catch 'robot-op
  (let ((status (motor-status motor)))
    (unwind-protect
        (progn (turn-on-motor motor)
               (manipulate motor))
      (when (motor-on? motor)
        (turn-off-motor motor))
      (setf (motor-status motor) status))))

I/O问题:使用FORMAT

  • 不要在格式化字符串(或任何用于输出的字符串)中使用Tab字符。根据输出从哪一列开始,制表符在输出上的排列可能与代码中的不一样!
  • 不要使用"#<~S ~A>"来打印不可读对象。应该使用print-unreadable-object
  • 考虑使用大写格式指令,使它们从周围的小写文本中脱颖而出。
    例如:“Foo: ~A” 而不是 “Foo: ~a”
  • 学习有用的惯用语。例如: ~{~A~^, ~}~:p
  • 注意何时使用~& versus ~%
    当然,“~2%” 和 "~2&"也有用。
    大部分输出到一个单独行的代码应该以~&开头,以~%结尾。
(format t "~&This is a test.~%")
This is a test.
  • 注意具体实现的扩展。它们可能不可移植,但对于不可移植的代码可能非常有用。例如,Genera的 用于处理缩进。

正确使用流

  • *standard-output**standard-input* 对比 *terminal-io*
    不要假定*standard-output**standard-input*会被绑定到*terminal-io*(或者,事实上,任何交互式流)。然而,你可以把它们绑定到这样一个流。
    不要尝试直接将*terminal-io*用于输入输出。它主要作为一个流,可以被其他流所绑定,或者可以间接连接(例如,通过同义流 synonym stream )。
  • *error-output* 对比 *debug-io*
    *error-output*用于没有任何用户交互的警告和错误提示。
    *debug-io*用于交互式的警告和错误提示,其他与程序正常功能无关的交互。
    特别是,不要期望它们是相同的流,首先在*error-output*上打印消息,然后在*debug-io*上执行调试会话。相反,在一个流上一致地进行每个交互。
  • *trace-output*
    这个不仅可以被用于接受trace的输出。如果您编写的调试例行程序在不停止正在运行的程序的情况下有条件地打印有用的信息,请考虑对该流进行输出,以便如果*trace-output*被重定向,您的调试输出也会被重定向。

一个有用的测试:如果有人只重新绑定了您正在使用的几个I/O流中的一个,这会使您的输出看起来很愚蠢吗?

猜你喜欢

转载自blog.csdn.net/zssrxt/article/details/133439284
今日推荐