自由定理

类型签名除了能够帮助我们推断函数可能的实现,还能够给我们带来自由定理(free theorems)。下面是两个直接从 Wadler 关于此主题的论文 中随机选择的例子:

  1. // head :: [a] -> a
  2. compose(f, head) == compose(head, map(f));
  3. // filter :: (a -> Bool) -> [a] -> [a]
  4. compose(map(f), filter(compose(p, f))) == compose(filter(p), map(f));

不用写一行代码你也能理解这些定理,它们直接来自于类型本身。第一个例子中,等式左边说的是,先获取数组的头部(译者注:即第一个元素),然后对它调用函数 f;等式右边说的是,先对数组中的每一个元素调用 f,然后再取其返回结果的头部。这两个表达式的作用是相等的,但是前者要快得多。

你可能会想,这不是常识么。但根据我的调查,计算机是没有常识的。实际上,计算机必须要有一种形式化方法来自动进行类似的代码优化。数学提供了这种方法,能够形式化直观的感觉,这无疑对死板的计算机逻辑非常有用。

第二个例子 filter 也是一样。等式左边是说,先组合 fp 检查哪些元素要过滤掉,然后再通过 map 实际调用 f(别忘了 filter 是不会改变数组中元素的,这就保证了 a 将保持不变);等式右边是说,先用 map 调用 f,然后再根据 p 过滤元素。这两者也是相等的。

以上只是两个例子,但它们传达的定理却是普适的,可以应用到所有的多态性类型签名上。在 JavaScript 中,你可以借助一些工具来声明重写规则,也可以直接使用 compose 函数来定义重写规则。总之,这么做的好处是显而易见且唾手可得的,可能性则是无限的。

类型约束

最后要注意的一点是,签名也可以把类型约束为一个特定的接口(interface)。

  1. // sort :: Ord a => [a] -> [a]

胖箭头左边表明的是这样一个事实:a 一定是个 Ord 对象。也就是说,a 必须要实现 Ord 接口。Ord 到底是什么?它是从哪来的?在一门强类型语言中,它可能就是一个自定义的接口,能够让不同的值排序。通过这种方式,我们不仅能够获取关于 a 的更多信息,了解 sort 函数具体要干什么,而且还能限制函数的作用范围。我们把这种接口声明叫做类型约束(type constraints)。

  1. // assertEqual :: (Eq a, Show a) => a -> a -> Assertion

这个例子中有两个约束:EqShow。它们保证了我们可以检查不同的 a 是否相等,并在有不相等的情况下打印出其中的差异。

我们将会在后面的章节中看到更多类型约束的例子,其含义也会更加清晰。

总结

Hindley-Milner 类型签名在函数式编程中无处不在,它们简单易读,写起来也不复杂。但仅仅凭签名就能理解整个程序还是有一定难度的,要想精通这个技能就更需要花点时间了。从这开始,我们将给每一行代码都加上类型签名。