自由定理
类型签名除了能够帮助我们推断函数可能的实现,还能够给我们带来自由定理(free theorems)。下面是两个直接从 Wadler 关于此主题的论文 中随机选择的例子:
// head :: [a] -> a
compose(f, head) == compose(head, map(f));
// filter :: (a -> Bool) -> [a] -> [a]
compose(map(f), filter(compose(p, f))) == compose(filter(p), map(f));
不用写一行代码你也能理解这些定理,它们直接来自于类型本身。第一个例子中,等式左边说的是,先获取数组的头部
(译者注:即第一个元素),然后对它调用函数 f
;等式右边说的是,先对数组中的每一个元素调用 f
,然后再取其返回结果的头部
。这两个表达式的作用是相等的,但是前者要快得多。
你可能会想,这不是常识么。但根据我的调查,计算机是没有常识的。实际上,计算机必须要有一种形式化方法来自动进行类似的代码优化。数学提供了这种方法,能够形式化直观的感觉,这无疑对死板的计算机逻辑非常有用。
第二个例子 filter
也是一样。等式左边是说,先组合 f
和 p
检查哪些元素要过滤掉,然后再通过 map
实际调用 f
(别忘了 filter
是不会改变数组中元素的,这就保证了 a
将保持不变);等式右边是说,先用 map
调用 f
,然后再根据 p
过滤元素。这两者也是相等的。
以上只是两个例子,但它们传达的定理却是普适的,可以应用到所有的多态性类型签名上。在 JavaScript 中,你可以借助一些工具来声明重写规则,也可以直接使用 compose
函数来定义重写规则。总之,这么做的好处是显而易见且唾手可得的,可能性则是无限的。
类型约束
最后要注意的一点是,签名也可以把类型约束为一个特定的接口(interface)。
// sort :: Ord a => [a] -> [a]
胖箭头左边表明的是这样一个事实:a
一定是个 Ord
对象。也就是说,a
必须要实现 Ord
接口。Ord
到底是什么?它是从哪来的?在一门强类型语言中,它可能就是一个自定义的接口,能够让不同的值排序。通过这种方式,我们不仅能够获取关于 a
的更多信息,了解 sort
函数具体要干什么,而且还能限制函数的作用范围。我们把这种接口声明叫做类型约束(type constraints)。
// assertEqual :: (Eq a, Show a) => a -> a -> Assertion
这个例子中有两个约束:Eq
和 Show
。它们保证了我们可以检查不同的 a
是否相等,并在有不相等的情况下打印出其中的差异。
我们将会在后面的章节中看到更多类型约束的例子,其含义也会更加清晰。
总结
Hindley-Milner 类型签名在函数式编程中无处不在,它们简单易读,写起来也不复杂。但仅仅凭签名就能理解整个程序还是有一定难度的,要想精通这个技能就更需要花点时间了。从这开始,我们将给每一行代码都加上类型签名。