Write Small Functions Using Examples
我们都想编写正确的函数,并证明它是正确的。这可以让我们带着问题思考一个函数的“大小”。这不是指函数实现的代码量(尽管那也很有趣)而是指我们代码所体现的数学函数的大小。
例如,在围棋游戏中有一个称为Atari的条件,玩家的棋子可能会被对手捕获:一颗棋子周围有两个及以上的空格即自由,否则即Atari。计算一颗棋子的自由度很困难,但推断它是否为Atari就很简单。我们可以写出类似这样的函数:
1 | boolean atari(int libertyCount) |
这比看起来大。数学函数可以被理解为一个集合,即笛卡尔积的某些子集的域(这里是int)以及范围(这里是boolean)。如果这些值的集合在Java中都有相同的大小,那么集合里就会有2L*(Interger.MAX_VALUE+(-1L*Integer.MIN_VALUE)+1L)或者8,598,934,592个元素在这个int x boolean的集合内。这些子集有一半是我们的函数,因此为了完全证明该函数是正确的,我们需要4.3x109个例子来检查。
这种主张的本质是说:测试无法证明没有bug。尽管测试可以示范现有功能,但任然存在这种大小问题。
上述问题域可以帮到我们。一个棋子的自由度不会是任何int型,确切地说之会是{1,2,3,4}中的一个。因此我们可以这么写:
1 | LibertyCount = {1, 2, 3, 4} |
这就简单多了:这个函数是一个最多包涵8个成员的集合。事实上,4个检查例子就能够形成该函数正确与否的完整的证明。这就是为什么最好选用与问题域紧密相连的类型而非原始类型的原因之一。使用受域限制的类型能让我们的函数更小。找出那些类型的方法是在编写函数之前在问题域中找出用于检查的示例。