[译] Hey Underscore, You're Doing It Wrong!

原视频信息


声明

原视频画面无字幕,主持人的讲话是在自动识别字幕基础上翻译。

代码 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)7add3(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() 改得更函数式一些。

  1. word 改成 _.first() 的最后一个参数。

    js 复制代码
    var firstTwoLetters = function(words) {
      return _.map(words, function(word) {
        return _.first(2, word);
      });
    }

    先传 2,这样 _.first(2) 返回的新函数就只等接收 word

  2. 简化 _.map() 的第二参数。

    js 复制代码
    var firstTwoLetters = function(words) {
      return _.map(words, _.first(2));
    }

    _.first(2) 直接替换掉刚才 _.map() 的回调函数。

  3. words 改成 _.map() 的最后一个参数。

    js 复制代码
    var firstTwoLetters = function(words) {
      return _.map(_.first(2), words);
    }
  4. 简化 firstTwoLetters() 的第二参数。

    js 复制代码
    var 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

    类型「一致」便于组合

组合

_.compose()

函数组合 ------ 把两个函数粘在一起得到一个新函数,它会从右到左运这行两个函数。

js 复制代码
//+ last :: [a] -> a
var last = function(xs) {
  var sx = reverse(xs);
  return first(sx);
}

last 获取数组最末元素。

  1. last() 先接收一个数组。
  2. 再反转数组。
  3. 最后返回首个元素。

赋值给变量,变量再传给函数,这种写法很容易想到,不过,它还能更高效。

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('&quote;', '"', html));
}

//+ createComment :: Html -> Comment
createComment = compose(Comment.create, replace('&quote;', '"'))
  1. replace('&quote;', '"') 返回一个待接收 Html 的新函数。
  2. 新函数的返回值再传给 Comment.create()

范畴论

Categroy Theory 范畴论

"The mathematical theory of transforming values and crap"

「变换宝物和废物的数学理论」

*inaccurate definition

*假装定义

来看这个例子,左、中俩圈儿代表类型 A,右边的是类型 B(Breakfast),gf 是函数。

如果 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, gf 不管哪俩先结合,最终组合出来的函数都一样,执行顺序都相同。

函数组合 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)

咱把 ArrayMyObject 都当成是盒子,知道这个意思就行,就别在意 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 上,会发生什么?

  1. 取出 MyObject() 里的值
  2. 将之传进 map() 的函数里
  3. 函数的返回值成为 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() 吧,别的例子里出现过。

  1. current_user 先传入第一个函数 getGreeting() 去读 name 属性值,比方 '某某某'
  2. 然后传入 concat('Welcome ') 返回 'Welcome 某某某'
  3. 最后传入 $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);
译者注

代码来源:underscore@1.6.0/underscore.js#L95

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,因为把参数列表反转了嘛,同时都自动柯里化。

以后还会大家分享更多函数式编程方面的东西,也希望咱们队伍能越来越壮大。

点赞关注不迷路,后会有期。

参考

OOP vs type classes

Footnotes

  1. Brian Lonsdorf

  2. point-free

  3. Type Class

  4. wu.js

  5. 开始翻译时 wu.js 版本为 v2.1.0,autoCurry() 已被替换为 curryable()

  6. 纯函数

相关推荐
大福是小强2 天前
002-Kotlin界面开发之Kotlin旋风之旅
kotlin·函数式编程·lambda·语法·运算符重载·扩展函数
再思即可4 天前
sicp每日一题[2.63-2.64]
算法·lisp·函数式编程·sicp·scheme
老章学编程i14 天前
Java函数式编程
java·开发语言·函数式编程·1024程序员节·lanmbda
安冬的码畜日常23 天前
【玩转 JS 函数式编程_014】4.1 JavaScript 纯函数的相关概念(下):纯函数的优势
开发语言·javascript·ecmascript·函数式编程·js·functional·原生js
Dylanioucn1 个月前
【编程进阶知识】Java 8 函数式编程接口全解析:Supplier、Runnable、Function、Consumer、Apply
java·开发语言·函数式编程
矢心1 个月前
函数式编程---js的链式调用理解与实现方法
前端·javascript·函数式编程
安冬的码畜日常1 个月前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
请不要叫我菜鸡1 个月前
mit6824-01-MapReduce详解
大数据·分布式·后端·mapreduce·函数式编程·容错性
安冬的码畜日常1 个月前
【玩转 JS 函数式编程_004】1.4 如何应对 JavaScript 的不同版本
开发语言·前端·javascript·ecmascript·函数式编程·fp·functional
再思即可1 个月前
sicp每日一题[2.31]
编程·lisp·函数式编程·sicp·scheme