单纯手打,方便学习回顾,原文请看
类型签名(type signatures)
类型(type)是让所有不同背景的人都能高效沟通的元语言。很大程度上,类型签名是以"HIndley-Milner"系统写就的。
类型签名在写纯函数时所起的作用非常大。这些签名轻述着函数最不可告人的秘密。短短一行,就能暴露函数的行为和目的。类型签名还衍生出了"自由定理(free theorems)"的概念。因为类型是可以推断的,所以明确的类型签名并不是必要的;不过你完全可以写精确度很高的类型签名,也可以让它们保持通用,抽象。类型签名不但可以用于编译时检测(compile time checks),还是最好的文档。所以类型签名在函数式编程中扮演着非常重要的角色--重要程度远远超过你的想象。
Hindley-Milner类型签名
// capitalize :: String -> String
var capitalize = function(s){
return toUpperCase(head(s)) + toLowerCase(tail(s));
}
capitalize("smurf");
// => "smurf"
这里,capitalize
接受一个 String
并返回了一个 String
。
在Hindley-Milner系统中,函数都写成类型a->b
这个样子,其中a
和b
是任意类型的变量。因此,capitalize
函数的类型签名可以理解成"一个接受String
返回String
的函数"。换句话说,它接受一个String
类型作为输入,并返回一个String
类型的输出。
// match :: Regex -> string -> [String]
var match = curry(function(reg,s){
return s.match(reg);
});
不过在还不完全了解细节的条件下,你尽可以把最后一个类型视作返回值。那么match
函数就可以这么理解: 它接受一个Regex
和一个String
,返回一个[String]
。
对于上面的match函数我们完全可以把它的类型签名这样分组:
//match :: Regex -> (String-> [String])
var match = curry(function(reg,s){
return s.match(reg);
});
把最后两个类型包在括号里就能反映更多的信息了。现在我们可以看出match
这个函数接受一个Regex
作为参数,返回一个从String
到[String]
的函数。因为 curry 造成的结果就是这样:给match
一个Regex
,得到一个新函数,能够处理其String
参数。
最后:
// id :: a -> a
var id = function(x){ return x; }
//map :: (a -> b) -> [a] -> [b]
var map = curry(function(f,xs)){
return xs.map(f);
});
这里的id
函数接受任意类型的a
并返回一个类型的数据。和普通代码一样,我们也可以在类型签名中使用变量。把变量命名为a
和b
只是一种约定俗成的习惯,你可以使用任何你喜欢的名称。对于相同的变量名,其类型也一定相同。这是非常重要的原则,所以我们必须重申:a -> b
可以是从任意类型中a
到任意类型b
,但是a -> a
必须是同一类型。例如,id
可以是String -> String
,也可以是Number -> Number
,但不能是String -> Bool
。
相似地,map
也使用了变量,只不过这里的b
可能与a
类型相同,也可能不相同。我们可以这么理解:map
接受两个参数,第一个是从任意类型a
到任意类型b
的函数;第二个是一个数组,元素是任意类型的a
;map
最后返回的是一个类型b
的数组。
// reduce :: (b -> a -> b) -> b -> [a] -> b
var reduce = curry(function(f,x,xs){
return xs.reduce(f, x);
});
"here goes nothing" , 可以看出它的第一个参数是一个函数,这个函数接受一个b
和一个a
并返回一个b
。那么这些a
和b
是从哪来的呢?很简单,签名中的第二个和第三个参数就是b
和元素为a
的数组,所以唯一合理的假设就是这里的b
和每一个a
都将传给前面说的函数作为参数。我们还可以看到,reduce
函数最后返回的结果是一个b
,也就是说,reduce
的第一个参数函数的输出就是reduce
函数的输出。
缩小可能性范围
一旦引入一个类型变量,就会出现一个奇怪的特性叫做:parametricity。这个特性表明,函数将会以一个统一的行为作用于所有的类型。
// head :: [a] -> a
注意看 head
,可以看到它接受[a]
返回a
。我们除了知道参数是个数组,其他一概不知;所以函数的功能就只限于操作这个数组上。在它对a
一无所知的情况下,它可能对a
进行什么操作呢?换句话说,a
告诉我们它不是一个特定的类型,这意味着它们可以是任意类型;那么我们的函数对每一个可能的类型的操作都必须保持统一。这就是parametricity的含义。要让我们来猜测head
的实现的话,唯一合理的推断就是它返回数组的第一个,或者最后一个,或者某个随机的元素;当然,head
这个命名应该能给我们一些线索。
自由定理
类型签名除了能够帮助我们判断函数可能的实现,还能够给我们带来自由定理(free theorems)。
// 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,然后再取其返回结果的头部,这两个表达式是相等的,但是前者要快得多。
你可能会想,这不是常识么,但计算机是没有常识的。实际上,计算机必须要有一种形式化方法来自动进行类似的代码优化。数学提供了这种方法,能够形式化直观的感觉,这无疑对死板的计算机逻辑非常有用。
类型约束
签名也可以把类型约束成一个特定的接口
// 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 类型签名在函数式编程中无处不在,它们简单易读,写起来也不复杂。但仅仅凭签名就能理解整个程序还是有一定难度的,要想精通这个技能就更要花点时间了。