原视频信息
- 视频地址:Hey Underscore, You're Doing It Wrong!
- 发布时间:2013/05/22
声明
原视频画面无字幕,主持人的讲话是在自动识别字幕基础上翻译。
代码 100% 还原,PPT 原文尽可能还原并翻译。
视频简介
Underscore 提供了部分当今函数式编程范式工具(像 map、filter、reduce、take、drop、compose 等),自诩有助于函数式编程。不过,有 ≠ 优。
为了优化,Brian Lonsdorf^1^ 支了些招儿。还帮函数式编程新手们,看代码,过基础,啃理论,讲概念。
幻灯片:mrkn.co/5nvjz
以前我在 HTML5DevConf 讲过 point-free^2^ 和 type class^3^,这次也能看见它俩。这次找几个 Underscore 的函数,新旧写法对比给大家演示。
议程
- 柯里化
- 组合
- 函子
- 侃
柯里化
Curried Function
函数柯里化
"A function that will return a new function until it receives all it's arguments"
「接收完参数前会一直返回新函数。」
js
//+ add :: Number -> Number -> Number
var add = function(x) {
return function(y) {
return x + y;
}
}
var add3 = add(3); // function(y){}
add3(4) // 7
add3(5) // 8
add(5)(3) // 8
函数 add()
的注释写的是类型签名。它接收参数 x
,返回新函数,新函数再接收 y
,返回 x + y
。
add(3)
叫做「对 add()
部分应用代入 3
」,然后得到新函数 add3()
。
add3(4)
得 7
,add3(5)
得 8
。
最后一行 add(5)(3)
只能这么写,因为不能自由传参。
Wu.js to the rescue!
快去请 Wu 来佛祖!
(well, one function at least)
(拿个函数请人家)
Wu.js^4^ 有个很棒的函数 ------ autoCurry()
^5^ 让你能自由传参。
js
var add = wu.autoCurry(function (a, b, c) { return a + b + c; });
add(1)(1)(1);
// 3
add(1)()(1)()(1);
// 3
>>> add(1)(1, 1);
// 3
add(1, 1, 1);
// 3
传了参数才能得到新函数,等传完就有最终结果了。
咱们把它加到 Function.prototype
,让函数都能自动柯里化。
js
//+ add :: Number -> Number -> Number
var add = function(x) {
return function(y) {
return x + y;
}
}.autoCurry();
var add3 = add(3); // function(y){}
add3(4) // 7
add3(5) // 8
add(3, 5) // 8
这下 add()
好用多了。
自由传参
js
//+ fullName :: String -> String -> String -> String
var fullName = function(first, middle, last) {
return first + ' ' + middle + ' ' + last;
}.autoCurry();
fullName("Hunter", "Stockton", "Thompson"); // Hunter Stockton Thompson
var billSomething = fullName("Bill") // function(middle,last){}
billSomething("Henry", "Cosby") // "Bill Henry Cosby"
var billJefferson = billSomething("Jefferson"); // function(last){}
billJefferson("Clinton") // "Bill Jefferson Clinton"
第 6 行,参数都传,得结果。
第 8 行,先只传第一参数,得到的新函数,第 10 行再调新函数传完参数,得到 "Bill Henry Cosby"。
巩固了自由传参,接下来看几个应用场景。
取余判断
js
//+ modulo :: Number -> Number -> Number
var modulo = function(divisor, dividend) {
return dividend % divisor;
}.autoCurry();
modulo(3,9) // 0
//+ isOdd :: Number -> Number
var isOdd = modulo(2);
isOdd(6) // 0
isOdd(5) // 1
取余函数 modulo()
先接收除数,再接收被除数。
modulo(2)
「要对 2 取余」,再传数字就是「指定数字对 2 取余」。
isOdd()
若返回 1
则视为 true
,即奇数。
谓词函数
js
//+ filter :: (a -> Bool) -> [a] -> [a]
var filter = function(f, xs) {
return xs.filter(f);
}.autoCurry();
filter(isOdd, [1,2,3,4,5]) // [1,3,5]
var getTheOdds = filter(isOdd);
getTheOdds([1,2,3,4,5]) // [1,3,5]
谓词函数传 isOdd()
用于选出数组里所有奇数。
filter(isOdd)
和 modulo(2)
一样,都会得到新函数。
参数顺序要合理
js
var firstTwoLetters = function(words) {
return _.map(words, function(word) {
return _.first(word, 2);
});
}
firstTwoLetters(['jim', 'kate']); //['ji', 'ka']
firstTwoLetters()
接收一个单词数组,返回各单词前两个字母。
第 2 行 _.map()
先接收单词数组,再接收一个回调函数。
第 3 行是回调函数的函数体内,返回各单词的前两个字母。
上面代码留作参考,咱把 firstTwoLetters()
改得更函数式一些。
-
word
改成_.first()
的最后一个参数。jsvar firstTwoLetters = function(words) { return _.map(words, function(word) { return _.first(2, word); }); }
先传
2
,这样_.first(2)
返回的新函数就只等接收word
。 -
简化
_.map()
的第二参数。jsvar firstTwoLetters = function(words) { return _.map(words, _.first(2)); }
_.first(2)
直接替换掉刚才_.map()
的回调函数。 -
words
改成_.map()
的最后一个参数。jsvar firstTwoLetters = function(words) { return _.map(_.first(2), words); }
-
简化
firstTwoLetters()
的第二参数。jsvar firstTwoLetters = _.map(_.first(2)); firstTwoLetters(['jim', 'kate']); //['ji', 'ka]
_.map(_.first(2))
返回的新函数就是需要传入words
的,所以直接赋值给firstTwoLetters
,最外层需要传words
的匿名函数也能让它拜拜了。
只要参数顺序合理,部分应用就能更好发挥威力。
Underscore 的函数要是能柯里化和部分应用,就能这样做。
js
var firstTwo = _.map(_.first(2));
firstTwo(['jim', 'kate']);
_.map(_.first(2), ['jim', 'kate']); //['ji', 'ka]
函数名都不需要 "Letters",或者直接调函数,_.first(2)
不就是 "firstTwo" 嘛,代码就能表明意图,words
这样的数据参数也省了。
underscore's api prevents you from currying
Underscore 的 API 和柯里化水火不容
总结
Currying 柯里化
Make generic functions - data is gone
没了数据参数,函数更通用
Build new functions by applying args
传入部分参数可用于定制新函数
Much more concise definitions
函数定义更简洁
Make types "line up" for composition
类型「一致」便于组合
组合
函数组合 ------ 把两个函数粘在一起得到一个新函数,它会从右到左运这行两个函数。
js
//+ last :: [a] -> a
var last = function(xs) {
var sx = reverse(xs);
return first(sx);
}
last
获取数组最末元素。
last()
先接收一个数组。- 再反转数组。
- 最后返回首个元素。
赋值给变量,变量再传给函数,这种写法很容易想到,不过,它还能更高效。
first()
和 reverse()
传入 compose()
就行。
js
//+ last :: [a] -> a
var last = compose(first, reverse);
last([1,2,3]) // 3
compose()
的运行顺序从右往左,每个函数的返回值,传给下一个函数。
函数类型要相同
js
//+ wordCount :: String -> Number
var wordCount = function(str) {
var words = split(' ', str);
return length(words);
}
//+ wordCount :: String -> Number
var wordCount = compose(length, split(' '));
wordCount("There are seven words in this sentence") // 7?
wordCount()
统计句子的单词个数。
咱们直接看第 8 行,一个函数的 返回类型 对应下一个函数的 输入类型 ,即 split(' ')
返回的新函数的 return type 对应 length()
的 input type ,因为函数类型相同,所以能组合。
对 柯里化 函数 部分应用 ,使函数类型相同进而能 组合。
解除嵌套
js
//+ createComment :: Html -> Comment
var createComment = function(html) {
return Comment.create(replace('"e;', '"', html));
}
//+ createComment :: Html -> Comment
createComment = compose(Comment.create, replace('"e;', '"'))
replace('"e;', '"')
返回一个待接收Html
的新函数。- 新函数的返回值再传给
Comment.create()
。
范畴论
Categroy Theory 范畴论
"The mathematical theory of transforming values and crap"
「变换宝物和废物的数学理论」
*inaccurate definition
*假装定义
来看这个例子,左、中俩圈儿代表类型 A,右边的是类型 B(Breakfast),g
和 f
是函数。
如果 g
是纯函数^6^,每次调用 g('John')
都返回 'Mary'
,g('Mary')
也一样,总是返回 'John'
。
'Mary'
传给 g()
,返回值再传给 f()
,最终得到 'eggs'
,顺着这条线能得出 f(g('Mary'))
。
既然一个函数的返回值能传给另一个函数,那这两个函数就能组合成一个新函数。中间圆圈就省了。
如同数学公式,似曾相识,咱可以把 <math xmlns="http://www.w3.org/1998/Math/MathML"> f ∘ g f \circ g </math>f∘g 看作一个公式,中间的圆就叫它 复合运算符 好了。
化繁为简,最终演变成,从 A 连到 D。
这能证明 结合律 ,h
, g
和 f
不管哪俩先结合,最终组合出来的函数都一样,执行顺序都相同。
函数组合 vs 链式调用
js
var sortedPhones = function(users) {
return _.chain(users)
.sortBy(function(user){ return user.signup_date; })
.map(function(user){ return user.phone })
.value();
}
sortedPhones(users);
既然叫 链式调用 ,那这「链」得有个头儿,先调 _.chain()
得到一个装盒了的对象,之后就能持续调方法,即 Underscore 的函数,最终再开盒,也就是在一系列方法调用的最后,调 _.value()
得到结果值。
js
var sortedPhones = compose(_.map(function(user){ return user.phnoe; })),
_.sortBy(function(user){ return user.signup_date });
sortedPhones(users);
函数组合 就是直接传函数。
js
var dot = function(prop, obj) {
return obj[prop];
}.autoCurry();
再写个函数 dot()
用于读属性。它接收一个属性 prop
和对象 obj
,返回读到的属性值。
js
var sortedPhones = compose(_.map(dot('phone')),
_.sortBy(dot('signup_date')));
sortedPhones(users);
更简洁。
相信大家已心领神会,之前按顺序下命令,现在先设计后实现,写好函数再按需组合。
共谋大业
万物皆函数,万事皆组合,我们上班也这么写,团队在招人,欢迎加入。
js
//+ alertAndClose :: Action(UI)
, alterAndClose = compose(closeWin, alertInvited)
//+ callApi :: InviteParams -> Promise(...)
, callAPi = compose(fmap(alterAndClose),
Repo.Student.inviteParents)
//+ inviteParents :: InviteParams -> Action(HTTP)
, inviteParents = compose(callApi, merge(user_id_param))
//+ extractParams :: [TextField] -> Field(InviteParams)
, extractParams = compose(mconcat, map(Field))
//+ sendInvite :: Field(Action(HTTP))
, sendInvite = compose(fmap(inviteParents),
extractParams,
getTextFields)
underscore promotes chain as the function of choice.
Underscore 推崇 链式调用。
函数组合 更得人心。
总结
Composition 组合
Build new functions from other functinos
用函数构建函数
Helps build generic programs w/o args
有助于构建无参数通用程序
Extremely high level coding
编程出神入化
Mathematically backed
数学上的支持
观众提问Q:能在 JavaScript 里用尾递归吗?
A:不推荐。一方面是递归没有组合高阶函数那样直接,在需要的时候再使用递归;另一方面,就算代码写了尾递归,浏览器也不会有尾递归优化的支持,你可以将循环包装起来,递归的模式抽取出来或者组合高阶函数。
Functors 函子
盒里
js
var plus1 = function(x){ return x + 1 }
plus([3])
// => Wrong!
map(plus1, [3])
// => [4]
plus1()
接收的是 值 ,如果想让 数组元素 +1,硬闯不行,如何应对?
对,有人想到了,用 map()
。
js
map(plus1, [3]) [plus1(3)]
//=> [4]
左右两边效果一样,plus1()
作用在数组元素。
js
map(plus1, Array(3)) Array(plus1(3))
//=> Array(4)
map(plus1, MyObject(3)) MyObject(plus1(3))
//=> MyObject(4)
咱把 Array
和 MyObject
都当成是盒子,知道这个意思就行,就别在意 Array()
这么调用实际上会返回啥了。
除了 Array
,能不能让 map()
也用在其他类型对象上?
实现
js
map(function(x){ return "I am" + x }, MyObject("yo"));
//=> MyObject("I am yo")
map(function(x){ return x.id }, MyObject({ id: 3 }));
//=> MyObject(3)
想象一下,让 map()
用在 MyObject
上,会发生什么?
- 取出
MyObject()
里的值 - 将之传进
map()
的函数里 - 函数的返回值成为
MyObject()
的新值
完全符合直觉。
js
map(plus1, MyObject(3)) MyObject(plus(3))
//=> MyObject(4)
MyObject = function(val) {
this.val = val;
}
MyObject.prototype.map = function(f) {
return MyObject(f(this.val));
}
假设构造函数 MyObject()
是这样,有个 val
属性引用值,再定义 map()
方法,像上面的注释那样,针对值运行函数 f()
。
对象是值的容器,我们映射对象,并在它的值上运行一个函数。
Functor 是一种有 map()
的接口,实现接口,就能将函数作用到对象中的值。
接下来演示几个有用的函子。
Maybe
js
map(plus1, Maybe(3))
//=> Maybe(4)
map(plus1, Maybe(null))
//=> Maybe(null)
Maybe = function(val) {
this.val = val;
}
Maybe.prototype.map = function(s) {
return this.val ? Maybe(f(this.val)) : Maybe(null);
}
Maybe
就是字面意思,可能有值,也可能没有。有值才运行函数,像第 4 行的 Maybe(null)
啥也不做。
检查 null
抽象成函子,类似于动态类型安全的东西。
Either
Either
接收俩参数,第一参数是默认值,第二参数是主要值。
js
map(plus1, Either(1, 2))
//=> Either(1, 3)
map(plus1, Either(1, null))
//=> Either(2, null)
Either = function(left, right) {
this.left = left;
this.right = right;
}
第 1 行的 Either
传了主要值,plus1()
就对 2
生效,返回 Either(1, 3)
。
第 4 行的主要值无效,plus1()
就对后备值起作用。
js
Either.prototype.map = function(f) {
return this.right ?
Either(this.left, f(this.right)) :
Either(f(this.left), null);
}
若值无效则使用后备值,这个逻辑都熟悉,Either
就是对它的抽象。
Promise
Promise
也能当成是 Functor。
js
map(populateTable, $.ajax.get('/posts'));
//=> Promise(...)
这行一看就明白,$.ajax.get('/posts')
获取贴子数据,再映射成 Promise<TableHtml>
,
js
Promise.prototype.map = function(f) {
var promise = new Promise();
this.then(function(response){
promise.resolve(f(response));
});
return promise;
}
车同轨,书同文,统一用 map()
好写也好记,.then()
,.when()
,.on()
一个 Promise 库一个样像话吗。
欢迎光临
js
$div = $("#myDiv");
//+ getGreeting :: User -> String
var getGreeting = compose(concat('Welcome '), dot('name'));
//+ updateGreetingHtml :: User -> undefined
var updateGreetingHtml = compose($div.html, getGreeting);
updateGreetingHtml(App.current_user);
// Welcome Bob
还记得 dot()
吧,别的例子里出现过。
current_user
先传入第一个函数getGreeting()
去读name
属性值,比方'某某某'
。- 然后传入
concat('Welcome ')
返回'Welcome 某某某'
。 - 最后传入
$div.html()
,更新指定元素的内容。
js
map(updateGreetingHtml, Maybe(App.current_user));
如果 current_user
无效,比方用户没登录,就派 Maybe
上场,不过 Maybe<User>
不能直接传给 updateGreetingHtml()
,还需要 map()
。
js
map(updateGreetingHtml, Either({ name: "blanky" }, App.current_user));
更优方案是用 Either
,如果 current_user
无效,就显示 'Welcome blanky'
。
js
map(updateGreetingHtml, Promise(App.current_user));
或者用 Promise
从数据库取用户数据也行。
第 9 行是程序主体,在它之上的行,都是纯逻辑的函数,和数据无关。仅在主体部分略微修改就能应对各种情况。
_.map()
很不爽,自己实现的接口不能用,因为 _.map()
的实现就是先试 Array.prototype.map()
,再用它那一套 _.each()
。
underscore explititly prevents extending map
Underscore 的
map()
油盐不进
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
译者注
Functors/Typeclasses 函子/类型类
-
Change behavior without altering function open/closed principle
不改变函数开闭原则的情况下改变行为
-
Not just map - reduce, compose, etc
除了 map,还有 reduce、compose 等等
-
Intuition and "non proprietary" api
直观地「非专有」api
-
Free formulas
自由形态
-
Dynamic type safety?
动态类型安全?
侃
我以前就在干这个事儿,在 Underscore 基础上改出一个库,叫 Scoreunder,因为把参数列表反转了嘛,同时都自动柯里化。
以后还会大家分享更多函数式编程方面的东西,也希望咱们队伍能越来越壮大。
点赞关注不迷路,后会有期。
参考
Footnotes
-
开始翻译时 wu.js 版本为 v2.1.0,
autoCurry()
已被替换为curryable()
↩