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

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

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

Hindley-Milner 类型签名

类型签名是一个非常常用的系统,我们可以从很多计算机语言系统上看到它的使用,下面来看个栗子:

//  capitalize :: String -> Stringvarcapitalize =function(s){returntoUpperCase(head(s)) + toLowerCase(tail(s));}capitalize("smurf");//=> "Smurf"复制代码

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

在 Hindley-Milner 系统中,函数都写成类似 a -> b 这个样子,其中 a 和 b 是任意类型的变量。因此, capitalize 函数的类型签名可以理解为“一个接受 String 返回 String 的函数”。换句话说,它接受一个 String 类型作为输入,并返回一个 String 类型的输出。

看看一些函数签名

//  strLength :: String -> NumbervarstrLength =function(s){returns.length;}//  join :: String -> [String] -> Stringvarjoin = curry(function(what, xs){returnxs.join(what);});//  match :: Regex -> String -> [String]varmatch = curry(function(reg, s){returns.match(reg);});//  replace :: Regex -> String -> String -> Stringvarreplace = curry(function(reg, sub, s){returns.replace(reg, sub);});复制代码

strLength 和 capitalize 类似:接受一个 String 然后返回一个 Number 。

具体来看看 match 函数

对于 match 函数,我们完全可以把它的类型签名这样分组:

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

是的,把最后两个类型包在括号里就能反映更多的信息了。现在我们可以看出 match 这个函数接受一个 Regex 作为参数,返回一个从 String 到 [String] 的函数。因为 curry ,造成的结果就是这样:给 match 函数一个 Regex ,得到一个新函数,能够处理其 String 参数。当然了,我们并非一定要这么看待这个过程,但这样思考有助于理解为何最后一个类型是返回值。

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

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

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

但是在这段代码中,就像你看到的那样,为 replace 加上这么多括号未免有些多余。所以这里的括号是完全可以省略的,如果我们愿意,可以一次性把所有的参数都传进来;所以,一种更简单的思路是: replace 接受三个参数,分别是 Regex 、 String 和另一个 String ,返回的还是一个 String 。

如果你使用过 TypeScript 来看看下面的改写

//  capitalize :: String -> Stringletcapitalize = (s:String):String=> {    toUpperCase(head(s)) + toLowerCase(tail(s));}//  match :: Regex -> (String -> [String])letmatch= curry((reg:RegExp, s:String): string[] =>{  s.match(reg);});复制代码

可以看到 TypeScript 的语法更加易于理解不需要注释大家应该也能明白输入和输出的类型,我们可以知道 TypeScript 是借鉴类类似于类型签名的思想去做的类型检测,以至于我们使用 JavaScript 的时候更加的方便。

具体的 TypeScript 基础的函数类型定义可以看看我的文章 TypeScript 基本类型和泛型的使用

缩小可能性范围 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] -> acompose(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 ,用了就回不去的好玩物。

进群:697699179可以获取Java各类入门学习资料!

这是我的微信公众号【编程study】各位大佬有空可以关注下,每天更新Java学习方法,感谢!

学习中遇到问题有不明白的地方,推荐加小编Java学习群:697699179内有视频教程 ,直播课程 ,等学习资料,期待你的加入

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,753评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,668评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,090评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,010评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,054评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,806评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,484评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,380评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,873评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,021评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,158评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,838评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,499评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,044评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,159评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,449评论 3 374
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,136评论 2 356

推荐阅读更多精彩内容