用于参数检查和其他偏执狂的常见 Lisp 习语?
Common Lisp idioms for argument checking and other paranoia?
这个问题是关于生产中的关键任务 Common-Lisp 代码的编码约定、最佳实践和风格。我仔细阅读了 Google 的 Common-Lisp 风格指南 (http://tinyurl.com/qfvnqcx),但没有找到任何明确解决我的具体问题的内容,我通过示例和对比 C/C++ 表达了这一点,Java,等等。我还快速浏览了基于 Github 的 Common-Lisp 代码,我没有看到很多参数检查和中间值检查,我在 C/C++, Java, 等等
在我的店里,我们非常习惯于检查参数和其他值,并在参数不符合合同/先决条件等时提前退出。例如,考虑以下(人为的、不完美的、典型的、但请不要浪费时间批评微示例,它预示着 CL 示例):
ErrorCode o_symb_to_g_symb (char * symb, uint len)
{ if (len < 2) { return ERROR_LENGTH; }
if (symb[0] != 'O' || symb[1] != '!') { return ERROR_SYNTAX; }
char * result = (char *) malloc (len + 1);
if (NULL == result) { return ERROR_MALLOC; }
if (result != strncpy (result, symb, len + 1))
{ return ERROR_STRNCPY; }
result[0] = 'G';
return result; }
这与 "Let Over Lambda," 第 67 页中 Doug Hoyte 的代码的作用大致相同,只是在整个过程中尽可能多地检查 (http://letoverlambda.com/)。
(defun o!-symbol-to-g!-symbol (s)
(symb "G!"
(subseq (symbol-name s) 2))))
问题是 Common Lisp 中的实际生产代码是否进行了更多检查。例如,编写明确的代码来检查 s 是否实际上是一个字符串并且实际上足够长,并且实际上有一个 "O!" 作为它的前两个字符可能是合理的。
这段代码是否仅仅因为它具有教学意义就绕过了所有这些偏执狂?关键任务生产部署中的相同代码是否更有可能进行偏执狂检查(我对 CL 代码的 Github 的轻描淡写表明 "no")?如果现实世界的 CL 代码不倾向于偏执,为什么不呢?极端案例或详尽测试的做法是否比看起来更普遍?
总之,我对风格的差异感到很困惑。现实世界中的关键任务 C 代码往往非常偏执。我在 CL 中看不到相同的内容。也许我没有查看正确的代码库?也许我没有读对的书?这个问题的答案似乎不太容易通过谷歌搜索找到。
Common Lisp 是一种专为开发大型复杂应用程序而设计的语言。在 80 年代被认为是大型应用程序。但它从生产系统中获得了几个处理错误的工具,甚至还支持编译时检查。还有很多代码是为原型软件、研究系统 and/or 个人目的而编写的。您并不总能找到高质量的产品。还要记住,有时非常严格的检查会使代码过于严格(例如:许多 HTTP 客户端会发送不符合要求的请求,但事实就是如此,并且不能在不失去大量潜在用户的情况下轻易拒绝它们).
让我们看一些 Common Lisp 如何帮助您编写健壮的软件的例子:
强类型和运行时间类型检查
我们希望普通的 Lisp 系统会对每个操作进行 运行时间检查。避免使用不支持的 Lisp 系统。
如果你有一个数字函数:
(defun foo (n x)
....
(bar ...))
(defun bar (a b)
(+ a b))
如果 FOO
不检查参数,我们预计 +
操作最终会检查参数。在 运行 时会出现错误,错误处理程序将 运行,默认情况下,它将调用调试器。
想一想:所有(大多数)操作都将在 运行 时检查。所有对象都有一个原始类型标记(整数、字符串、数组、位向量、字符、流...),并且在 运行 时间最终会检查类型。
但我们对 Lisp 的期望更高 运行time:
- 数组边界检查
- 插槽类型检查
- 错误情况下的堆一致性
- 针对重新定义标准函数、删除 Common Lisp 包、算术错误等有害操作的各种检查
使用不进行 运行时间类型检查的 Lisp 系统是一个巨大的痛苦。现在,Common Lisp 允许我们声明部分代码不进行 运行 时间检查。最佳策略:找到可以在不产生风险的情况下完成的最少代码量(参见 LOCALLY
)。
参数列表
Common Lisp 允许在编译时检查一些参数列表。使用它。
(defun foo (&key (n 1) (x 1.0))
...)
现在,典型的编译器会捕获类似 (foo :y 2 :x 2.0)
的调用,但会出现错误:错误的关键字参数 :y
.
让编译器检查参数列表是否包含正确数量的参数以及是否使用了正确的关键字参数。
CLOS,Common Lisp 对象系统
使用 CLOS。
(defmethod foo ((n integer) (x float)) ...)
如果您像上面那样定义一个方法,在 运行 时方法体中 n
将是一个整数,而 x
将是一个浮点数。如果您使用其他参数类型调用 FOO
并且没有应用任何方法,那么我们会收到 运行 时间错误。
与实例插槽类似:您可以声明类型。
(defclass bar ()
((x :type float)
(n :type integer)))
使用实际检查这些声明的 Common Lisp 实现或编写您自己的检查。
另外:不要创建基于列表的原始数据结构。始终将它们打包到 CLOS 类 和方法中。这样您就可以获得适量的 运行 时间检查和自省功能。
在运行时间
检查类型
Common Lisp 提供了一个用于运行时间类型检查的宏:CHECK-TYPE.
(defun foo (n x)
(check-type n integer)
(check-type x float)
(* (isqrt n) (sqrt x)))
CHECK-TYPE
宏允许花哨的类型检查甚至修复错误。
CL-USER 27 > (foo 2000 5)
Error: The value 5 of X is not of type FLOAT.
1 (continue) Supply a new value of X.
2 (abort) Return to level 0.
3 Return to top loop level 0.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.
CL-USER 28 : 1 > :c 1
Enter a form to be evaluated: 5.0
请注意,您可以使用类型来指定数字的间隔、数组维度或类似内容。
例如,这会检查绑定到变量 a1
的对象是否是维度为 3 x 3 的二维数组:
(check-type a1 (array * (3 3)))
请注意,您可以使用带有任意类型谓词的 DEFTYPE
定义自己的类型。
使用 Lisp 构造信号错误
例如 ecase
与 case
:
CL-USER 37 > (let ((code 10))
(ecase code
(1 'fine)))
Error: 10 fell through ECASE expression.
Wanted one of (1).
ecase
在没有子句匹配时自动发出错误信号。
ASSERT
宏允许我们检查任意断言。
Common Lisp 提供了一个内置的 ASSERT 宏。
(defun foo (n x)
(assert (and (integerp n) (evenp n)) (n))
(assert (floatp x) (x))
(* (isqrt n) (sqrt x)))
同样,一定数量的运行时间修复可用:
CL-USER 33 > (foo 2001 5.0)
Error: The assertion (AND (INTEGERP N) (EVENP N)) failed.
1 (continue) Retry assertion with new value for N.
2 (abort) Return to level 0.
3 Return to top loop level 0.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.
CL-USER 34 : 1 > :c 1
Enter a form to be evaluated:
2000
98.38699
使用 CLOS 进行简单的按合同设计
(defclass bar ()
((n :type integer)
(x :type float)))
(defmethod setup-bar ((b bar) (n1 integer) (x1 float))
(with-slots (n x) b
(setf n n1 x x1))
b))
现在我们可以编写一个额外的方法来检查 n
是否大于 x
:
(defmethod setup-bar :before ((b bar) (n1 integer) (x1 float))
(assert (> n x) (n x)))
:before 方法将始终 运行 before 主要方法。
向 CLOS 添加合同设计系统
有相应的库。 Quid Pro Quo is an example. There is also a simpler and older DBC implementation by Matthias Hölzl: Design by Contract.
条件系统的高级错误处理
写入条件类型:
(define-condition mailer-incomplete-delivery-error
(mailer-error)
((recipient-and-status-list :initarg :recipient-and-status-list
:reader mailer-error-recipient-and-status-list)))
以上是基于mailer-error
条件的新条件。在 运行 时间,我们可以捕获 SMTP 响应代码并发出这种情况的信号。
编写处理程序并重新启动以处理错误。那是先进的。条件系统的广泛使用通常表示更好的代码。
编写并检查测试
在许多情况下,健壮的代码需要一个测试套件。 Common Lisp也不例外。
让用户报错
在许多 Common Lisp 实现中,可以获得错误条件对象、回溯和一些环境数据。将这些写入错误日志。让用户报告那些。例如 LispWorks 在调试器中有 :bug-form
命令。
这个问题是关于生产中的关键任务 Common-Lisp 代码的编码约定、最佳实践和风格。我仔细阅读了 Google 的 Common-Lisp 风格指南 (http://tinyurl.com/qfvnqcx),但没有找到任何明确解决我的具体问题的内容,我通过示例和对比 C/C++ 表达了这一点,Java,等等。我还快速浏览了基于 Github 的 Common-Lisp 代码,我没有看到很多参数检查和中间值检查,我在 C/C++, Java, 等等
在我的店里,我们非常习惯于检查参数和其他值,并在参数不符合合同/先决条件等时提前退出。例如,考虑以下(人为的、不完美的、典型的、但请不要浪费时间批评微示例,它预示着 CL 示例):
ErrorCode o_symb_to_g_symb (char * symb, uint len)
{ if (len < 2) { return ERROR_LENGTH; }
if (symb[0] != 'O' || symb[1] != '!') { return ERROR_SYNTAX; }
char * result = (char *) malloc (len + 1);
if (NULL == result) { return ERROR_MALLOC; }
if (result != strncpy (result, symb, len + 1))
{ return ERROR_STRNCPY; }
result[0] = 'G';
return result; }
这与 "Let Over Lambda," 第 67 页中 Doug Hoyte 的代码的作用大致相同,只是在整个过程中尽可能多地检查 (http://letoverlambda.com/)。
(defun o!-symbol-to-g!-symbol (s)
(symb "G!"
(subseq (symbol-name s) 2))))
问题是 Common Lisp 中的实际生产代码是否进行了更多检查。例如,编写明确的代码来检查 s 是否实际上是一个字符串并且实际上足够长,并且实际上有一个 "O!" 作为它的前两个字符可能是合理的。
这段代码是否仅仅因为它具有教学意义就绕过了所有这些偏执狂?关键任务生产部署中的相同代码是否更有可能进行偏执狂检查(我对 CL 代码的 Github 的轻描淡写表明 "no")?如果现实世界的 CL 代码不倾向于偏执,为什么不呢?极端案例或详尽测试的做法是否比看起来更普遍?
总之,我对风格的差异感到很困惑。现实世界中的关键任务 C 代码往往非常偏执。我在 CL 中看不到相同的内容。也许我没有查看正确的代码库?也许我没有读对的书?这个问题的答案似乎不太容易通过谷歌搜索找到。
Common Lisp 是一种专为开发大型复杂应用程序而设计的语言。在 80 年代被认为是大型应用程序。但它从生产系统中获得了几个处理错误的工具,甚至还支持编译时检查。还有很多代码是为原型软件、研究系统 and/or 个人目的而编写的。您并不总能找到高质量的产品。还要记住,有时非常严格的检查会使代码过于严格(例如:许多 HTTP 客户端会发送不符合要求的请求,但事实就是如此,并且不能在不失去大量潜在用户的情况下轻易拒绝它们).
让我们看一些 Common Lisp 如何帮助您编写健壮的软件的例子:
强类型和运行时间类型检查
我们希望普通的 Lisp 系统会对每个操作进行 运行时间检查。避免使用不支持的 Lisp 系统。
如果你有一个数字函数:
(defun foo (n x)
....
(bar ...))
(defun bar (a b)
(+ a b))
如果 FOO
不检查参数,我们预计 +
操作最终会检查参数。在 运行 时会出现错误,错误处理程序将 运行,默认情况下,它将调用调试器。
想一想:所有(大多数)操作都将在 运行 时检查。所有对象都有一个原始类型标记(整数、字符串、数组、位向量、字符、流...),并且在 运行 时间最终会检查类型。
但我们对 Lisp 的期望更高 运行time:
- 数组边界检查
- 插槽类型检查
- 错误情况下的堆一致性
- 针对重新定义标准函数、删除 Common Lisp 包、算术错误等有害操作的各种检查
使用不进行 运行时间类型检查的 Lisp 系统是一个巨大的痛苦。现在,Common Lisp 允许我们声明部分代码不进行 运行 时间检查。最佳策略:找到可以在不产生风险的情况下完成的最少代码量(参见 LOCALLY
)。
参数列表
Common Lisp 允许在编译时检查一些参数列表。使用它。
(defun foo (&key (n 1) (x 1.0))
...)
现在,典型的编译器会捕获类似 (foo :y 2 :x 2.0)
的调用,但会出现错误:错误的关键字参数 :y
.
让编译器检查参数列表是否包含正确数量的参数以及是否使用了正确的关键字参数。
CLOS,Common Lisp 对象系统
使用 CLOS。
(defmethod foo ((n integer) (x float)) ...)
如果您像上面那样定义一个方法,在 运行 时方法体中 n
将是一个整数,而 x
将是一个浮点数。如果您使用其他参数类型调用 FOO
并且没有应用任何方法,那么我们会收到 运行 时间错误。
与实例插槽类似:您可以声明类型。
(defclass bar ()
((x :type float)
(n :type integer)))
使用实际检查这些声明的 Common Lisp 实现或编写您自己的检查。
另外:不要创建基于列表的原始数据结构。始终将它们打包到 CLOS 类 和方法中。这样您就可以获得适量的 运行 时间检查和自省功能。
在运行时间
检查类型Common Lisp 提供了一个用于运行时间类型检查的宏:CHECK-TYPE.
(defun foo (n x)
(check-type n integer)
(check-type x float)
(* (isqrt n) (sqrt x)))
CHECK-TYPE
宏允许花哨的类型检查甚至修复错误。
CL-USER 27 > (foo 2000 5)
Error: The value 5 of X is not of type FLOAT.
1 (continue) Supply a new value of X.
2 (abort) Return to level 0.
3 Return to top loop level 0.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.
CL-USER 28 : 1 > :c 1
Enter a form to be evaluated: 5.0
请注意,您可以使用类型来指定数字的间隔、数组维度或类似内容。
例如,这会检查绑定到变量 a1
的对象是否是维度为 3 x 3 的二维数组:
(check-type a1 (array * (3 3)))
请注意,您可以使用带有任意类型谓词的 DEFTYPE
定义自己的类型。
使用 Lisp 构造信号错误
例如 ecase
与 case
:
CL-USER 37 > (let ((code 10))
(ecase code
(1 'fine)))
Error: 10 fell through ECASE expression.
Wanted one of (1).
ecase
在没有子句匹配时自动发出错误信号。
ASSERT
宏允许我们检查任意断言。
Common Lisp 提供了一个内置的 ASSERT 宏。
(defun foo (n x)
(assert (and (integerp n) (evenp n)) (n))
(assert (floatp x) (x))
(* (isqrt n) (sqrt x)))
同样,一定数量的运行时间修复可用:
CL-USER 33 > (foo 2001 5.0)
Error: The assertion (AND (INTEGERP N) (EVENP N)) failed.
1 (continue) Retry assertion with new value for N.
2 (abort) Return to level 0.
3 Return to top loop level 0.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.
CL-USER 34 : 1 > :c 1
Enter a form to be evaluated:
2000
98.38699
使用 CLOS 进行简单的按合同设计
(defclass bar ()
((n :type integer)
(x :type float)))
(defmethod setup-bar ((b bar) (n1 integer) (x1 float))
(with-slots (n x) b
(setf n n1 x x1))
b))
现在我们可以编写一个额外的方法来检查 n
是否大于 x
:
(defmethod setup-bar :before ((b bar) (n1 integer) (x1 float))
(assert (> n x) (n x)))
:before 方法将始终 运行 before 主要方法。
向 CLOS 添加合同设计系统
有相应的库。 Quid Pro Quo is an example. There is also a simpler and older DBC implementation by Matthias Hölzl: Design by Contract.
条件系统的高级错误处理
写入条件类型:
(define-condition mailer-incomplete-delivery-error
(mailer-error)
((recipient-and-status-list :initarg :recipient-and-status-list
:reader mailer-error-recipient-and-status-list)))
以上是基于mailer-error
条件的新条件。在 运行 时间,我们可以捕获 SMTP 响应代码并发出这种情况的信号。
编写处理程序并重新启动以处理错误。那是先进的。条件系统的广泛使用通常表示更好的代码。
编写并检查测试
在许多情况下,健壮的代码需要一个测试套件。 Common Lisp也不例外。
让用户报错
在许多 Common Lisp 实现中,可以获得错误条件对象、回溯和一些环境数据。将这些写入错误日志。让用户报告那些。例如 LispWorks 在调试器中有 :bug-form
命令。