用于参数检查和其他偏执狂的常见Lisp习语?

Reb*_*bin 17 design-by-contract common-lisp

这个问题涉及编码约定,最佳实践和生产风格,关键任务Common-Lisp代码.我仔细阅读了Google的Common-Lisp样式指南(http://tinyurl.com/qfvnqcx),但没有找到任何明确解决我特定问题的内容,我通过示例表达,与C/C++,Java等形​​成对比.我还快速浏览了Github上的Common-Lisp代码库,我没有看到很多参数检查和中间值检查,我在C/C++,Java等中看到过.

在我的商店里,我们非常习惯于检查论点和其他价值观,并在论证不符合合同/先决条件等时提前退出.例如,考虑以下(设计,不完美,典型,但请 - 不要' t-waste-time-criticizing,micro-example,它预示着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;   }
Run Code Online (Sandbox Code Playgroud)

这与Doug Hoyte的第67页"Let Over Lambda"的代码大致相同,只是在整个过程中尽可能地检查(http://letoverlambda.com/).

  (defun o!-symbol-to-g!-symbol (s)
    (symb "G!"
          (subseq (symbol-name s) 2))))
Run Code Online (Sandbox Code Playgroud)

问题是Common Lisp中的实际生产代码是否进行了更多检查.例如,编写显式代码以检查s实际上是一个字符串并且实际上足够长并且实际上具有"O!"可能是合理的.作为前两个字符.

这段代码是否因为教学方法而绕过所有的偏执狂?在任务关键型生产部署中,相同的代码是否更有可能进行偏执检查(我对Github的CL代码的轻量级代表会建议"不")?如果现实世界的CL代码不倾向于偏执狂,为什么不呢?角落案例或详尽测试的做法是否比看上去更广泛?

简而言之,我对风格的差异感到非常困惑.真实世界的任务关键型C代码往往是超级偏执狂.我在CL中看不到相同的内容.也许我不是在寻找合适的代码库?也许我没看过合适的书?Googling似乎并不容易找到这个问题的答案.

Rai*_*wig 23

Common Lisp是一种用于开发大型和复杂应用程序的语言.80年代被认为是大型应用.但它从生产系统中获得了几个处理错误的工具,甚至还有一些支持编译时检查的工具.仍然有很多代码是为原型软件,研究系统和/或个人目的而编写的.你总是找不到高质量的产品.还要记住,有时非常严格的检查会使代码过于严格(例如:许多HTTP客户端会发送不符合要求的请求,但就是这样,人们不能轻易拒绝它们而不会丢失大量可能的用户).

让我们看一下Common Lisp如何帮助您编写健壮的软件:

强类型和运行时类型检查

我们希望通常的Lisp系统会对每个操作进行运行时检查.避免使用没有的Lisp系统.

如果你有一个数字函数:

(defun foo (n x)
  ....
    (bar ...))

(defun bar (a b)
  (+ a b))
Run Code Online (Sandbox Code Playgroud)

如果FOO没有参数检查,我们期望最终+操作将检查参数.在运行时会出现错误并运行错误处理程序,默认情况下会调用调试程序.

想一想:所有(大多数)操作都将在运行时进行检查.所有对象都有一个基本类型标记(整数,字符串,数组,位向量,字符,流,...),并且在运行时最终将检查该类型.

但是我们期望从Lisp运行时获得更多:

  • 数组边界检查
  • 插槽类型检查
  • 错误时的堆一致性
  • 针对有害操作的各种检查,例如重新定义标准函数,删除Common Lisp包,算术错误等.

使用不执行运行时类型检查的Lisp系统是一个巨大的痛苦.现在,Common Lisp允许我们声明代码的一部分不进行运行时检查.最佳策略:找到可以在不产生风险的情况下完成最少量的代码(请参阅参考资料LOCALLY).

参数列表

Common Lisp允许在编译时进行一些参数列表检查.用它.

(defun foo (&key (n 1) (x 1.0))
  ...)
Run Code Online (Sandbox Code Playgroud)

现在,典型的编译器将捕获类似于(foo :y 2 :x 2.0)错误的调用:错误的关键字参数:y.

让编译器检查参数列表是否具有正确数量的参数,以及正在使用正确的关键字参数.

CLOS,Common Lisp对象系统

使用CLOS.

(defmethod foo ((n integer) (x float)) ...)
Run Code Online (Sandbox Code Playgroud)

如果你定义一个像上面这样的方法,那么在运行时,方法体n中将是一个整数,并且x将是一个浮点数.如果FOO使用其他参数类型调用并且没有应用任何方法,则会出现运行时错误.

类似于插槽:您可以声明类型.

(defclass bar ()
   ((x :type float)
    (n :type integer)))
Run Code Online (Sandbox Code Playgroud)

使用Common Lisp实现,它实际检查这些声明或编写您自己的检查.

另外:不要基于列表创建原始数据结构.始终将它们打包到CLOS类和方法中.这样您就可以获得适当数量的运行时检查和内省功能.

在运行时检查类型

Common Lisp为运行时类型检查提供了一个宏:CHECK-TYPE.

(defun foo (n x)
  (check-type n integer)
  (check-type x float)
  (* (isqrt n) (sqrt x)))
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

请注意,您可以使用类型来指定数字间隔,数组维度或类似内容.

例如,这将检查绑定到变量的对象a1是尺寸为3乘3的二维数组:

(check-type a1 (array * (3 3)))
Run Code Online (Sandbox Code Playgroud)

请注意,您可以DEFTYPE使用任意类型谓词定义自己的类型.

使用Lisp构造信号错误

例如ecase主场迎战case:

CL-USER 37 > (let ((code 10))
               (ecase code
                 (1 'fine)))

Error: 10 fell through ECASE expression.
Wanted one of (1).
Run Code Online (Sandbox Code Playgroud)

ecase 当没有子句匹配时,自动发出错误信号.

ASSERT宏允许我们检查任意断言.

Common Lisp提供了一个内置的ASSERT宏.

(defun foo (n x)
  (assert (and (integerp n) (evenp n)) (n))
  (assert (floatp x) (x))
  (* (isqrt n) (sqrt x)))
Run Code Online (Sandbox Code Playgroud)

同样,可以使用一定量的运行时修复:

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
Run Code Online (Sandbox Code Playgroud)

使用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))
Run Code Online (Sandbox Code Playgroud)

现在我们可以编写一个额外的方法来检查例如n大于x:

(defmethod setup-bar :before ((b bar) (n1 integer) (x1 float))
   (assert (> n x) (n x)))
Run Code Online (Sandbox Code Playgroud)

:以前方法将总是运行的主要方法.

将按合同设计的系统添加到CLOS

有图书馆.Quid Pro Quo就是一个例子.MatthiasHölzl还有一个更简单,更老的DBC实现:契约式设计.

条件系统的高级错误处理

写条件类型:

(define-condition mailer-incomplete-delivery-error
          (mailer-error)
  ((recipient-and-status-list :initarg :recipient-and-status-list
                  :reader mailer-error-recipient-and-status-list)))
Run Code Online (Sandbox Code Playgroud)

以上是基于条件的新mailer-error条件.在运行时,我们可以捕获SMTP响应代码并发出这样的信号.

编写处理程序并重新启动以处理错误.那是先进的.条件系统的广泛使用通常表示更好的代码.

编写并检查测试

在许多情况下,健壮的代码需要测试套件.Common Lisp也不例外.

让用户报告错误

在许多Common Lisp实现中,可以获得错误条件对象,回溯和一些环境数据.将它们写入错误日志.让用户报告这些.例如,LispWorks :bug-form在调试器中有命令.

  • 我可能会补充:CHECK-TYPE断言还可以帮助编译器优化代码.许多CL编译器都"聪明",足以推断出,如果`x`已通过`(check-type x(整数0 10))`,那么他们可以优化该函数中的后续代码,仅用于从0到10的整数通常,你从这些声明中获胜两次. (2认同)