[译] 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. 纯函数

相关推荐
Oberon11 天前
从零开始的函数式编程(2) —— Church Boolean 编码
数学·函数式编程·λ演算
桦说编程20 天前
CompletableFuture 超时功能有大坑!使用不当直接生产事故!
java·性能优化·函数式编程·并发编程
桦说编程23 天前
如何安全发布 CompletableFuture ?Java9新增方法分析
java·性能优化·函数式编程·并发编程
桦说编程1 个月前
【异步编程实战】如何实现超时功能(以CompletableFuture为例)
java·性能优化·函数式编程·并发编程
鱼樱前端1 个月前
Vue3之ref 实现源码深度解读
vue.js·前端框架·函数式编程
RJiazhen2 个月前
前端项目中的函数式编程初步实践
前端·函数式编程
再思即可3 个月前
sicp每日一题[2.77]
算法·lisp·函数式编程·sicp·scheme
桦说编程3 个月前
把 CompletableFuture 当做 monad 使用的潜在问题与改进
后端·设计模式·函数式编程
蜗牛快跑2133 个月前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
大福是小强4 个月前
002-Kotlin界面开发之Kotlin旋风之旅
kotlin·函数式编程·lambda·语法·运算符重载·扩展函数