了解 JavaScript 函数式编程-类型签名

了解 JavaScript 函数式编程目录

类型签名

初识类型

JavaScript 是一种动态的类型语言,但这并不意味着要否认类型的使用。咱们平常打交道的主要就是字符串、数值、布尔值等。虽然 JavaScript 语言成面上没有相关的集成。不过咱们可使用类型签名生成文档,也可使用注释帮助咱们区分类型。编程

有些朋友应该使用过一些 JavaScript 类型检查工具,好比 Flow 或者 是其余的静态类型检测语言类如 TypeScript。api

Hindley-Milner 类型签名

类型签名是一个很是经常使用的系统,咱们能够从不少计算机语言系统上看到它的使用,下面来看个栗子:数组

// capitalize :: String -> String
var capitalize = function(s){
  return toUpperCase(head(s)) + toLowerCase(tail(s));
}

capitalize("smurf");
//=> "Smurf"
复制代码

这里的 capitalize 接受一个 String 并返回了一个 String。这里咱们不关心实现函数过程,咱们只关注它的类型签名bash

Hindley-Milner 系统中,函数都写成相似 a -> b 这个样子,其中 ab 是任意类型的变量。所以,capitalize 函数的类型签名能够理解为“一个接受 String 返回 String 的函数”。换句话说,它接受一个 String 类型做为输入,并返回一个 String 类型的输出。函数式编程

看看一些函数签名

// strLength :: String -> Number
var strLength = function(s){
  return s.length;
}

// join :: String -> [String] -> String
var join = curry(function(what, xs){
  return xs.join(what);
});

// match :: Regex -> String -> [String]
var match = curry(function(reg, s){
  return s.match(reg);
});

// replace :: Regex -> String -> String -> String
var replace = curry(function(reg, sub, s){
  return s.replace(reg, sub);
});

复制代码

strLengthcapitalize 相似:接受一个 String 而后返回一个 Number函数

具体来看看 match 函数

对于 match 函数,咱们彻底能够把它的类型签名这样分组:工具

// match :: Regex -> (String -> [String])
var match = curry(function(reg, s){
  return s.match(reg);
});
复制代码

是的,把最后两个类型包在括号里就能反映更多的信息了。如今咱们能够看出 match 这个函数接受一个 Regex 做为参数,返回一个从 String[String] 的函数。由于curry,形成的结果就是这样:给 match 函数一个 Regex,获得一个新函数,可以处理其 String 参数。固然了,咱们并不是必定要这么看待这个过程,但这样思考有助于理解为什么最后一个类型是返回值。post

// match :: Regex -> (String -> [String])

// onHoliday :: String -> [String]
var onHoliday = match(/holiday/ig);
复制代码

每传一个参数,就会弹出类型签名最前面的那个类型。因此 onHoliday 就是已经有了 Regex 参数的 matchui

// replace :: Regex -> (String -> (String -> String))
var replace = curry(function(reg, sub, s){
  return s.replace(reg, sub);
});
复制代码

可是在这段代码中,就像你看到的那样,为 replace 加上这么多括号未免有些多余。因此这里的括号是彻底能够省略的,若是咱们愿意,能够一次性把全部的参数都传进来;因此,一种更简单的思路是:replace 接受三个参数,分别是 RegexString 和另外一个 String,返回的仍是一个 String搜索引擎

若是你使用过 TypeScript 来看看下面的改写

// capitalize :: String -> String
let capitalize = (s: String): String => {
    toUpperCase(head(s)) + toLowerCase(tail(s));
}

// match :: Regex -> (String -> [String])
let match = curry((reg:RegExp, s:String): string[] =>{
   s.match(reg);
});

复制代码

能够看到 TypeScript 的语法更加易于理解不须要注释你们应该也能明白输入和输出的类型,咱们能够知道 TypeScript 是借鉴类相似于类型签名的思想去作的类型检测,以致于咱们使用 JavaScript 的时候更加的方便。

缩小可能性范围 narrowing of possibility

一旦引入一个类型变量,就会出现一个奇怪的特性叫作 parametricity(en.wikipedia.org/wiki/Parame… )。这个特性代表,函数将会以一种统一的行为做用于全部的类型。咱们来研究下:

// head :: [a] -> a
复制代码

注意看 head,能够看到它接受 [a] 返回a。咱们除了知道参数是个数组,其余的一律不知;因此函数的功能就只限于操做这个数组上。在它对 a 一无所知的状况下,它可能对 a 作什么操做呢?换句话说,a 告诉咱们它不是一个特定的类型,这意味着它能够是任意类型;那么咱们的函数对每个可能的类型的操做都必须保持统一。这就是 parametricity 的含义。要让咱们来猜想 head 的实现的话,惟一合理的推断就是它返回数组的第一个,或者最后一个,或者某个随机的元素;固然,head 这个命名应该能给咱们一些线索。 再看一个例子:

// reverse :: [a] -> [a]
复制代码

仅从类型签名来看,reverse 可能的目的是什么?再次强调,它不能对 a 作任何特定的事情。它不能把 a 变成另外一个类型,或者引入一个 b;这都是不可能的。那它能够排序么?答案是不能,没有足够的信息让它去为每个可能的类型排序。它能从新排列么?能够的,我以为它能够,但它必须以一种可预料的方式达成目标。另外,它也有可能删除或者重复某一个元素。重点是,无论在哪一种状况下,类型 a 的多态性(polymorphism)都会大幅缩小 reverse 函数可能的行为的范围。

这种“可能性范围的缩小”(narrowing of possibility)容许咱们利用相似 Hoogle 这样的类型签名搜索引擎去搜索咱们想要的函数。类型签名所能包含的信息量真的很是大。

自由定理 free theorems

类型签名除了可以帮助咱们推断函数可能的实现,还可以给咱们带来自由定理(free theorems)。来看一个栗子

// head :: [a] -> a
compose(f, head) == compose(head, map(f));
复制代码

例子中,等式左边说的是,先获取数组的第一个元素,而后对它调用函数 f;等式右边说的是,先对数组中的每个元素调用 f,而后再取其返回结果的头部。这两个表达式的做用是相等的,可是前者要快得多。

在 JavaScript 中,你能够借助一些工具来声明重写规则,也能够直接使用 compose 函数来定义重写规则。总之,这么作的好处是显而易见且唾手可得的,可能性则是无限的。若是这里不太明白 compose 的使用的话,能够翻到前面看看 code 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 类型签名在函数式编程中无处不在,它们简单易读,写起来也不复杂。但仅仅凭签名就能理解整个程序仍是有必定难度的,要想精通这个技能就更须要花点时间了。固然如今是推荐你们使用 TypeScript,用了就回不去的好玩物。

相关文章
相关标签/搜索