函数式编程中的组合子

函数式编程是一个比较大的话题,里面的知识体系很是的丰富,在这里我并不想讲的特别的详细。为了应对实际中的应用,咱们讲一下函数式编程中最为实用的应用方式——组合子。组合子自己是一种高阶函数,他的特色就是将函数进行延迟或者转换,在函数式编程中应用最为普遍。

什么是组合子

组合子在数学中就有,但咱们讲的并非数学中的定义,而是在JavaScript领域中的组合子概念。按照我所理解的JavaScript函数式编程,我将组合子分为辅助组合子函数组合子。后续咱们会对这两种组合子进行区别。javascript

组合子(全称:组合子函数)又称之为装饰器函数,用于转换函数或数据,加强函数或数据行为的高阶函数。这里咱们提到的高阶函数并不陌生,所谓的高阶函数,就是以函数为参数或者返回值的函数。辅助组合子是最为简单的组合子,它是具备数据流程控制的抽象函数。而函数组合子就很特别了,它必须以函数(称之为原函数契函数)为参数,其大体有以下特色:css

  1. 函数组合子自己就是高阶函数
  2. 不改变原函数契函数)的最终意图;
  3. 能加强原函数(契函数)的行为;

图片描述

图片描述

高阶函数的概念咱们已经很熟悉了,这里不作多的解释,咱们只强调,函数组合子是以原函数为参数,返回新函数高阶函数。知道这一点,咱们再来解释后面两个特性,所谓“不改变原函数的最终意图”,即原函数是作什么的,新函数就是作什么的。原函数新函数的所需参数是一致的,返回值也是同样的。这里咱们先卖个关子,稍后咱们会用实际的案例来说述一下这个特性。“能加强原函数的行为”,这是函数式编程的核心概念之一,他并不难理解,但须要咱们花更多的时间去关注。html

那么什么是函数的行为,为何要加强函数的行为。函数有三个重要的部分:输入、处理、输出。输入就是指的参数,而处理就是函数体中对参数的执行过程,输出就是返回值。在JavaScript语言中,即便函数没有输出,都约定输出的是undefined,以上都是咱们很是熟悉的概念。前端

参数便是“元”(arity)的,分为:一元unary)、二元binary)、三元ternary)、多元polyadic)、可变元variadic)。除了一元函数,其余的参数都或多或少存在着这样的两个问题:java

  1. 参数的获取时机;
  2. 参数的获取顺序;

图片描述

以咱们最常使用的ajax.get为例子,该函数有4个参数,最常使用的是其中的三元用法:git

/**
 * @param {String} url - 请求地址
 * @param {*} data - 请求参数
 * @param {Function} success - 成功回调函数
 */
$.get(url, data, success(response));

对于success回调函数,咱们由于知道数据获取的格式以及目标转换格式,所以咱们能够很快的构建这个回调。但若是url地址是要从DOM上获取,或者从其余资源文件中读取,那么该函数的执行与否,会更多的依赖url的获取与否,甚至还要考虑异步的问题。github

组合子就是要解决这个问题,但这里咱们不着急解决刚才提出的例子,由于在讲解组合子以前,咱们还要铺垫的说到函数式编程中比较重要的三个概念:柯里化偏函数应用函数组合ajax

柯里化

若是函数的参数足够多,而我又不肯定函数参数是否能在同一时间所有获取到,那么在执行这个函数前,总要等待用户的输入所有完成时,才能执行。而在等待以前,任何一处参数的获取时间将会影响后续过程的执行:spring

var url = getUrl(); // 若是这句耗时过长,将致使后面很难被执行到
var data = getData();
var callback = function (res) {};
$.get(url, data, callback);

若是不是一次性输入完成,为什么不返回一个新的函数来等待用户的下一次输入呢?柯里化很轻蔑的说了这么一句,因而它这样作:数据库

var curryGet = url => data => callback => $.get(url, data, callback);
curryGet(url)(data)(callback);

是的,你没看错,这简直像魔法同样,原来JavaScript中的函数还能这么玩。为了照顾不少没有学习ES6+的同窗,咱们直接用ES5的语法来书写。为了保证一致性,后面都将采用ES5的语法,特别状况下我会用ES6+来从新描述。那么刚才的柯里化代码用ES5写就是:

var currGet = function (url) {
  return function (data) {
    return function (callback) {
      return $.get(url, data, callback);
    }
  }
}

天啊,是否是已经看晕了,是否是有小伙伴火烧眉毛的想去学习ES6+了呢?但这里原理很简单,只是用到了名为闭包的魔法。原先依靠逗号分隔的参数,如今要一次次的输入,而且输入完最后一个方可执行。在不少同窗看来,这样作并不高明,增长了不少function的包裹不说,运行的结果和以前没有区别。是的,但他没有任何意义?

这里咱们并不打算详细研究函数编程的性能问题,这是一个很大的话题,在架构中也是有取有舍的。我只能说,整体性能上不必定比原来的差,某些场景下的优化空间还会比传统方式更大。你们只管放心使用便可,后续会对函数式编程优化进行专题讲演的。

柯里化做为一种最简单的组合子之一,他是一种高阶函数(显然这里咱们没有用到柯里化组合子),没有改变原有函数的意图,但却延迟了函数的执行。

偏函数应用

若是在编写代码时,咱们总能预知datacallback的肯定性和及时性,url须要最后代入,那么能够考虑使用偏函数的魔法,仍是刚才的例子,咱们能够这样的改造:

var partialGet = function (fn, data, callback) {
  return function (url) {
    return fn(url, data, callback);
  }
};
var partialGetByUrl = partialGet($.get, data, callback);
partialGetByUrl(url1);
partialGetByUrl(url2);
// ...

看,如今咱们已经实现了函数的重用了,而且咱们并无改造原有的函数,仅仅对原函数进行了改造。它的做用和柯里化很是的类似,但没有柯里化那么贪婪。偏函数应用仅仅是提取原函数中的部分参数,用剩余参数返回一个新函数。不难发现,偏函数柯里化都是延迟了原函数的参数,只是延迟的进度不一样而已。

函数组合

仍然接着上面的例子,若是url是从DOM中获取的原始输入,彷佛咱们为了合理性和安全性,应该对url进行一个预处理过程,大体能够假设有这样的代码:

var preformat = function (url) {};  // 预处理
partialGetByUrl(preformat(url));

这样写彷佛一点问题都没有,咱们再来增长点难度:

var trim = function (txt) {};  // 去除两边空格
var encode = function (txt) {};  // 编码加密
var preformat = function (url) {};  // 预处理
partialGetByUrl(encode(preformat(trim(url))));

天啊,我相信你已经也看晕了,更愚蠢的是,若是处理字符串的顺序变化了,改动也是很头疼的。面对这样的问题,伟大的组合函数出现了:

var compose = function (f4, f3, f2, f1) {
  return function (txt) {
    return f4(f3(f2(f1(txt))));
  }
};
var getByUrl = compose(
    partialGetByUrl,
  encode,
  preformat,
  trim
);
getByUrl(url1);
getByUrl(url2);

书写上好看了很多,而且你能够为所欲为的去组合这些函数的,固然这是有一些前提的(纯函数数据不变性),但我不打算在这里讲解这些前提。

这时有不少人很困惑,貌似组合函数对于组合子的特性前两点都是知足的,惟独第三点看似不像。注意了,加强的函数行为不只仅有延迟,组合串联也是一种加强手段,前一个函数会由于后一个函数而加强。甚至你能够换一种理解方式,若是没有后一个函数的提早处理就会致使前一个函数执行失败,这也是一种加强手段。

辅助组合子

说了那么多,前面说到的都是针对特定问题的高阶函数解决方案,抛开先前说的三个特性,如今回来咱们以前组合子的话题,首先讲讲最为简单的辅助组合子,它们自己不处理函数,只是处理数据,所以能够称之为辅助组合子(或者说是投影函数(Projecting Function))。但其实辅助组合子自己并非不处理函数,而是函数也能够做为特殊的数据,他们虽然小、写法简单,可是意义仍然和组合子同样重大:

// ES5
function nothing () {}
function identity (val) {
  return val;
}
function defaultTo (def) {
  return function (val) {
    return val || def;
  }
}
function always (constant) {
  return function () {
    return constant;
  }
}

// ES6+
const nothing = () => {};
const identity = val => val;
const defaultTo = def => val => val || def;
const always = cons => val => cons;

无为(nothing)

图片描述

nothing函数表面上看好像没有什么意义,但它的名称就和它自己同样,不做任何事情,是空函数的默认值,在ES6+的场景中比较常见,好比稍后将见到的alt组合子,就能够进行默认值传参:

const alt = (f1 = nothing, f2 = nothing) => val => f1(val) || f2(val);

照旧(identity)

图片描述

identity函数是范畴论中很是著名的id函子,一样有关函子的概念不是本文的重点,有兴趣的朋友能够自行找资料学习。id函子有着一个很是重要的特性,也就是输入什么值都将不经处理的返回,即便输入的是函数也是可行的。咱们能够利用这个特性在递归中使用,用构建后继传递递归(CPS)版的斐波那契数:

// n >= 1
const fib = (n, cont = identity) => 
    n <= 1 ? 
  cont(n) : 
    fib(n - 2, pre => fib(n - 1, mid => cont(pre + mid)));
注意:这段代码要想解读清楚比较困难,咱们只须要知道这个函数的结果是对的便可。

默许(defaultTo)

图片描述

defaultTo函数的出场率是最高的,好比咱们处理一些非预期值的时候:

const defaultLikeArray = defaultTo([]);
const array1 = defaultLikeArray('');
const array2 = defaultLikeArray([1, 2, 3]);

array1由于不是预期值,所会返回一个空的数组,防止该参数代入后续函数中后出现问题。defaultTo是一种OR组合子,后续还有更多相似的组合子出现。

图片描述

恒定(always)

图片描述

always函数不少人看不大懂,认为画蛇添足,直接用const关键字构建一个常量不就能够了吗。这就是函数式编程的特色,一切以函数为中心。该函数经过函数式的方式,构建了某些数据的统一源:

const alwaysUser = always({});
const user1 = alwaysUser();
const user2 = alwaysUser();
user1 === user2; // => true
user1.name = '张三';
user1 === user2; // => true

function alwaysLikeUser () {
  return {};
}
const user3 = alwaysLikeUser();
const user4 = alwaysLikeUser();
user3 == user4; // false

像这样,你就能保证不一样位置的数据修改是针对的统一源头,某些场景下仍是很是实用的。

函数组合子

柯里化偏函数应用函数组合的例子中咱们能够看到函数组合子的身影,但函数组合子自己更具有抽象性,他是这些特定问题的抽象,而且能够复用在绝大部分(只要符合条件)的函数身上。下面,咱们就来说解一下常见的函数组合子

收缩(gather)

/**
 * 函数签名: ((a, b, c, …, n) → x) → [a, b, c, …, n] → x
 * 函数做用: 参数收缩
 * 函数特性: 降维
 * @param fn - 原函数
 * @returns 参数收缩后的新函数
 */

// ES5
function gather (fn) {
  return function (argsArr) {
    return fn.apply(null, argsArr)
  }
}

// ES6+
const gather = fn => argsArr => fn(...argsArr);

// eg: 将`可变元`函数转换为`一元`函数
var log = gather(console.log);
log(['标题', 'log', '日志类容']); // => 标题 log 日志类容
var max = gather(Math.max);
max([1,2,3]); // => 3

图片描述

绝大部分人看到此组合子后,会很敏感的认为,这不就是Function.prototype.apply操做么?是的,gather函数就是封装的Function.prototype.apply,可是它并不关心数据自己,换句说法,它并不关心放进来的是什么函数(函数参数的抽象)。咱们常常会在别的函数体中调用某个函数的callapply方法,但不多有人尝试将该方法进行封装。gather函数只关注函数的行为,而不关注函数自己是什么,你传入任意的函数均可以。gather函数的目的是将可变元参数的函数转换为一元的,这是一种降维操做,能够适配用户的输入。被建立的新函数会经过gather的逆运算,将[a, b, c, ...]结构的数据转换为(a, b, c, ...)结构的数据再代入原函数

展开(spread)

/**
 * 函数签名: ([a, b, c, …, n] → x) → (a, b, c, …, n) → x
 * 函数做用: 参数展开
 * 函数特性: 升维
 * @param fn - 原函数
 * @returns 参数展开后的新函数
 */

// ES5
function spread (fn) {
  return function () {
    var argsArr = [].slice.call(arguments);
    return fn(argsArr)
  }
}

// ES6+
const spread = fn => (...argsArr) => fn(argsArr);

// eg: 将`一元`函数转换为`可变元`函数
var promiseAll = spread(Promise.all);
promiseAll(promiseA, promiseB, promiseC);

图片描述

spread函数与gather函数是对称的,一般他们会相互配合的使用,在后续的juxt案例中就能够看到。它的目的可使得原函数参数进行升维操做,由一元进化为可变元,形如(a, b, c, ...)的参数通过spread的逆运算会转变为[a, b, c, …]形式再代入原函数

值得注意的是,spread(gather)identity是等效的(不是相等==),你能够用几个可变元和多元函数来实验一下。

spread(gather(console.log))(1, 2, 3); // => 1, 2, 3
identity(console.log)(1, 2, 3); // => 1, 2, 3

颠倒(reverse)

/**
 * 函数签名: ((a, b, c, …, n) → x) → (n, …, c, b, a) → x
 * 函数做用: 参数倒序
 * 函数特性: 换序
 * @param fn - 原函数
 * @returns 参数收缩后的新函数
 */

// ES5
function reverse (fn) {
  return function argsReversed () {
    var args = [].reverse.call(arguments);
    return fn.apply(null, args);
  }
}

// ES6+
const reverse = fn => (...argsArr) => fn(...argsArr.reverse());

// eg: 将`多元`函数的参数反转为`多元`函数
var pipe = reverse(compose);
pipe(
  trim,
  format,
  encode,
  request
)(url);

图片描述

reverse组合子也是抽象了函数做为数据,能将任意的函数的参数“反转”,注意,它并非真的将函数参数反转了,而是生成了一个等待反转参数输入的新函数。

一样, reverse(reverse)idengtity也是等效的,你能够自行尝试

左偏(partial)

/**
 * 函数签名: ((a, b, c, …, n) → x) → [a, b, c, …] → ((d, e, f, …, n) → x)
 * 函数做用: 前置参数提早
 * 函数特性: 降维
 * @param fn - 原函数
 * @returns 前置参数提早后的新函数
 */

// ES5
function partial (fn) {
  var presetArgs = [].slice.call(arguments, 1);
  return function () {
    var laterArgs = [].slice.call(arguments);
    return fn.apply(
      null, 
      presetArgs.concat(laterArgs)
    );
  }
}

// ES6+
const partial = (fn, ...presetArgs) => (...laterArgs) => fn(...presetArgs, ...laterArgs);

// eg: 将`多元`函数转换成比先前更少元的`多元`函数
var getByHandler = partial($.get, url, data);
getByHandler(console.log);
getByHandler(convert);
getByHandler(render);

图片描述

partial函数就是一种偏函数应用(Partial Application),它能够提取函数中的一个或多个参数,但不是所有参数,构造出一个新函数。一样它能够下降原函数(契函数)的维度,使得函数的调用被延迟。之因此称之为““函数应用,是对应于彻底函数应用的称呼。那么什么是”彻底函数应用“呢?先看以下代码:

// 假设咱们抽离出一个map函数
cost map = (arr, transfomer) => [].map.call(arr, transfomer);
// 那么正常的调用过程就是`彻底函数应用`
map([1, 2, 3], x => x + 1);
// 提取部分参数构成新函数的过程就是`偏函数应用`
const mapWith = partial(map, [1, 2, 3]);
mapWith(x => x + 1);
// 固然你能够手动提取后面的参数
const withHandler = (fn, handler) => arr => fn(arr, handler);
const mapWithAddOne = withHandler(map, x => x + 1);
mapWithAddOne([1, 2, 3]); // => [2, 3, 4]
mapWithAddOne([-1, 0, 1]); // => [0, 1, 2]

偏函数应用能够延迟函数的执行,把真正关注的数据放在最后,从而实现函数的可复用性。本小节主要介绍的是左偏,与之对应的还有右偏

右偏(partialRight)

/**
 * 函数签名: ((a, b, c, …, n) → x) → [d, e, …, n] → ((a, b, c, …) → x)
 * 函数做用: 前置参数提早
 * 函数特性: 降维
 * @param fn - 原函数
 * @returns 前置参数提早后的新函数
 */

// ES5
function partialRight (fn) {
  var laterArgs = [].slice.call(arguments, 1);
  return function () {
    var presetArgs = [].slice.call(arguments);
    return fn.apply(
      null, 
      presetArgs.concat(laterArgs)
    );
  }
}

// ES6+
const partialRight = (fn, laterArgs) => (...presetArgs) => fn(...presetArgs, ...laterArgs);

// eg: 将`多元`函数转换成比先前更少元的`多元`函数
var getByUrl = partialRight($.getJSON, [data, render]);
getByUrl(url1);
getByUrl(url2);
getByUrl(url3);

图片描述

partialRight函数的思想是和partial如出一辙的,甚至咱们利用咱们已经学会的reversepartial函数转换成partialRight函数,有兴趣的同窗能够自行尝试一下,稍后咱们也会给出答案。

注意: 学到这里的时候,咱们不难发现,从 gatherpartial都是针对参数的维度变化。虽然他们都是能够代入函数为参数,但对函数特性是有要求的,好比 升维的前提必须是数组。再一个,咱们已经能够利用他们在不改变原有函数的前提下,组合出各类适合使用需求的函数。

柯里化(curry)

/**
 * 函数签名: (* → a) → (* → a)
 * 函数做用: 逐个参数提早
 * 函数特性: 降维
 * @param {Function} fn 原函数
 * @param {Number} arity 原函数的参数个数, 默认值: 原函数的参数个数
 * @returns 柯里化后的下一个`一元`函数
 */

// ES5
function curry (fn, arity) {
  arity = arity || fn.length;
  return (function nextCurried (prevArgs) {
    return function curried (nextArg) {
      var args = prevArgs.concat(nextArg);
      return args.length >= arity ? fn.apply(null, args) : nextCurried(args);
    }
  })([]);
}

// ES6+
const curry = (fn, arity = fn.length) => {
  return (function nextCurried (prevArgs) {
    return function curried (nextArg) {
      let args = prevArgs.concat(nextArg);
      return args.length >= arity ? fn(...args) : nextCurried(args);
    };
  })([]);
}

// eg: 
var square = curry(reverse(Math.pow))(2);
square(2); // => 4
square(3); // => 9

图片描述

不少人看到这里,以为curry拆分参数没什么用,反而让原来一个函数变成了多个函数,增长了JS脚本引擎的解析难度(调用栈增长)。函数式编程强调的是一种编程的范式,它是一种代码哲学,也是一种编程艺术,但切不可为了函数式而函数式,这会让本来的开发过程变得繁重。并且,函数式编程确实存在必定的性能上的优化空间(但这不是咱们本次讲稿所要叙述的内容),但也不能由于这一点而彻底避讳使用它,咱们应该根据环境要求去选择更加适当的组合方式。

再次回到curry函数,好比有以下案例:

// 未柯里化,这是不少人都会举的例子
const add = (a, b) => a + b;
add(1, 2); // => 3
// 柯里化后
const addCurry = curry(add);
const addOne = addCurry(1);
[1, 2, 3].map(addOne);

curry在原有函数add的基础上,进行了延拓,咱们无需去从新封装一个新的函数来描述addOne的特性,由于addOne的特性自己就是add部分具象化。这即是在鼓励咱们,去编写更抽象的函数,而后使用函数式编程使其具象化而且可复用。

curry函数自己的特性,与partial也是极为类似的,只不过它拆分的更加细腻,是逐个拆分。然而,问题也暴露出来了,若是函数是不定元的,那么curry又该如何保证正常使用呢?一块儿看看curry函数的定义,发现第二个参数默认是取原函数的参数长度。咱们知道,JavaScript中的Function能够经过length属性来获取参数的长度,例如:

console.log.length; // => 1
Math.sin.length; // => 1
Math.random.length; // => 0
JSON.parse.length; // => 2

console.log函数的参数长度虽然和Math.sin函数的长度同样,可是log函数能够再添加可变元的参数的,若是对log使用curry魔法,将致使施了魔法的函数和原函数彻底一致。所以,为了解决curry函数自己的缺陷,咱们能够手动创建一个新函数,用于指定函数参数的个数:

// 方式一,不使用组合子
const curry2Log = curry(console.log, 2);
const curry3Log = curry(console.log, 3);
curry2Log('title')('message'); // => title message
curry3Log('title')('message')('2018'); // => title message 2018
// 方式二,使用组合子(偏函数)
const curryN = (n, f) => partialRight(curry, [n]);
// 或者(反转函数)
const curryN = reverse(curry);
const curry4Log = curryN(4, console.log);
const curry5Log = curryN(5, console.log);

看,万变不离其宗,是否是很是的神奇,就像咱们介绍的同样——像魔法,因此大家是否是对他们愈来愈感兴趣了呢?

到此,咱们已经讲解了有关参数维护变化的组合子,大家发现了没, gatherspreadreversepartialcurry都是基于参数维度的变化。咱们已经在文中提过不少次 参数维度,一元函数是一维的,二元函数是二维的……多元函数是多维的,可变元函数是不定维的。这些组合子的特色就是改变维度,让 原函数能够变化成其它可复用的方式。固然,除了这些组合子能够改变维度,还有像 unarybinarynAry等组合子能够强行改变维度,好比 unary是将任意函数变为一元的,若是自己就是二元以上的函数,是会有损失的。即使你很难摸清楚维度切换的法门,也不用担忧,函数式编程的精妙在于,当你须要的时候,你就知道怎么去选择了,咱们所要作的是掌握和了解更多的组合子。

弃离(tap)

/**
 * 函数签名: (a → *) → a → a
 * 函数做用: 对输入值执行给定函数并当即返回输入值
 * 函数特性: id
 * @param {Function} fn - 原函数
 * @returns 输入值
 */

// ES5
function tap (fn) {
  return function (val) {
    return (fn(val), val);
  }
}

// ES6+
const tap = fn => val => (fn(val), val);

// eg:
var sayX = x => console.log('x is ' + x);
var tapSayX = R.tap(sayX);
tapSayX(100); // 100

tap函数相似一个ididentity,以后咱们都简称id)的组合子,函数自己作什么并不关心,咱们都没有接受它的返回值。那么它的用处是干吗呢?函数式编程中有一个很是重要的概念叫纯函数,这个词并不陌生,但很难甄别,先来看看下面哪些函数是“纯”的:

const addOne = x => x + 1;
const log = console.log;
const clickHandler = function (e) {
  e.preventDefault();
  $(this).html($(e.target).html());
};
var count = 1;
function getCount () {
  return count;
}
function append (arr, item) {
  arr.push(item);
  return arr;
}

还有不少的例子就很少举例了,上面的函数,除了addOne,其它的都不算纯,咱们来看纯函数应该具有的特性:

  1. 独立性:没有反作用,不会影响外部,也不受外部影响;
  2. 常恒性:官方说法叫引用透明性(Referential Transparency),即在任意时间里,传入相同且肯定的参数,返回相同且肯定的值;

简单的说,真正的纯函数是“永恒”和“不变”的。再回头看上面的案例,clickHandler使用了this,该函数会由于上下文的变化而做用不一样(独立性和常恒性被打破)。getCount引用了外部变量,这也是很是危险的,由于你没法保证变量a永远没法被其余人改变(常恒性被打破)。append传入的参数arr是一个引用类型,所以函数体内部对外部产生的反作用(独立性被打破)。有的人认为console.log是一个纯函数,是的,若是不考虑那么严格的话,它确实是一个比较纯的函数,但它和DOM操做内存操做写库操做(都属于I/O操做)同样,对外部产生了变化,所以它也不是一个纯函数(独立性)。但log操做也很特殊,由于DOM操做内存操做写库操做和它的区别就在于他们三个都存在并发,所以结果是不肯定的;是存在异常的,异常会中断函数自己的运行;是不可预测的,虽然咱们知道大部分都能正确返回,可不得不认可对结果预测的不稳定性。再看看log,虽然对外部(控制台)有I/O操做,但它既不存在并发,也不会有异常发生,结果是可预测的。所以,log能够认为是一个非严格意义上的纯函数,毕竟查看数据时它是很是有帮助的。

虽然 addOne函数咱们认识是一个 的,但若是传入的不是一个 基础类型,而是一个引用类型呢?那就不是的,由于内部的改变是影响了外部。这样的需求是时常发生的,那么又改如何编写函数式所须要的纯函数呢?这就须要咱们使用一些函数式的类库了,例如 RamdaImmutableRamda让全部传入的参数对象都会通过 clonedeepClone操做,使之引用关系被断开,从而产生新的对象返回; Immutable能够构造出具有 持久性不变性的数据结构,从而剥离反作用。本讲稿也是推荐各位使用出名的函数式编程库,而不要本身再重复造轮子的去构造这些 组合子,咱们只是借用这些例子来说解他们的特性和实际用途。

说了那么多纯函数,这和tap函数究竟有什么关系,和以后的组合子又有什么关系呢?tap函数就是用来隔离那些不纯的操做(实际使用时应加入clonedeepClone),保留原始数据流通到下一个关口,它能够用于辅助函数式编程进行数据调试。例如:

const getByHandler = partial($.ajax, [url, data]);
getByHandler(pipe(    // 假设获取到的数据是一个对象数组
    sortByField,  // 排序
  map(changeKey('_guid', 'id')),  // 将数据库的字段`_guid`切换为`id`
  tap(
      console.log // 将上一步的操做结果打印,并使数据经过
    // 甚至能够发起一个入库操做,让中间数据持久化
  ),
  renderData    //渲染数据到DOM中
));

通过map的数据到了tap以后,并无发生变化,就转而到了renderData去进行渲染了,log函数只是简单的将上层数据进行了打印。但若是tap内的函数是一个不纯的怎么办?咱们的tap函数还缺乏一个重要的辅助函数deepClone,也就是数据进去时只传递副本,这样就能有效避免灾难的发生。包扩咱们先后写的代码,都没有一些著名的库写的完备、高性能且安全,咱们只是经过这些代码示意来展现函数式编程的魅力。你们只要知道,通过tap函数的数据会直接返回,而tap包裹的函数使用完成后会直接弃用。

交替(alt)

/**
 * 函数签名: (a → x) -> (b → y) → v → x || y
 * 函数做用: 对输入值执行给定两个函数并返回不为空的结果
 * 函数特性: or
 * @param {Function} f1 - 原处理函数
 * @param {Function} f2 - 二次处理函数
 * @returns 不为空的结果值
 */

// ES5
function nothing () {}
function alt (f1, f2) {
  var f1 = f1 || nothing;
  var f2 = f2 || nothing;
  return function (val) {
    return f1(val) || f2(val);
  }
}

// ES6+
const nothing = () => {};
const alt = (f1 = nothing, f2 = nothing) => val => f1(val) || f2(val);

// eg:
var getFromDB = () => {};
var getFromCache = () => {};
var getData = alt(getFromDB, getFromCache);
getData(query);

图片描述

alt函数是最简单的一种OR组合子,它描述的就是程序语言中的if-else,只不过它并非用特定的表达式去判断,而是用||符号。OR组合子的变种有不少,像Ramda.js中的ifElseunlesswhencond都有相似逻辑功能,但更为丰富一些。

补救(tryCatch)

/**
 * 函数签名: (a → x) → (b → y) → v → x || y
 * 函数做用: 对输入值执行`tryer`函数,若无异常则直接返回处理结果,反之返回`catcher`处理后的结果
 * 函数特性: or
 * @param {Function} tryer - 处理函数
 * @param {Function} catcher - 补救函数
 * @returns 不为异常的结果值
 */

//  ES5
function tryCatch (tryer, catcher) {
  return function (val) {
    try {
      return tryer(val);
    }catch (e) {
      return catcher(val);
    }
  }
}

// ES6+
const tryCatch = (tryer, catcher) => val => {
  try {
    return tryer(val);
  }catch (e) {
    return catcher(val);
  }
};

// eg:
var toJson = tryCatch(JSON.parse, defaultTo({}));
var toArray = tryCatch(JSON.parse, defaultTo([]));
toJson('{"a": 1}'); // => {a: 1}
toArray(''); // []

tryCatch函数也是一种OR组合子,它和全部的类OR组合子同样,目的就是实现非此即彼

同时(seq)

/**
 * 函数签名: (a → x, b → y, …,) → val → undefined
 * 函数做用: 对输入值执行给定的全部函数
 * 函数特性: fork
 */

// ES5
function seq () {
  var fns = [].slice.call(arguments);
  return function (val) {
    for (var i = 0; i < fns.length; i++) {
      fns(val);
    }
  }
}

// ES6+
const seq = (...fns) => val => fns.forEach(fn => fn(val));

// eg: ajax请求成功后,作三件事
// 1. render 将请求到的数据渲染到页面上;
// 2. cache 将数据缓存到前端数据库中;
// 3. log 写一段日志,打印请求到的数据,方便控制台观测;
var ajaxSuccessHandler = seq(render, cache, log);

图片描述

seq组合子是一种分流操做,它的实际用途正如案例中所描述的,能够同时作一些事情。虽然代码是同步的版本,但也很容易用学到的知识去建立异步版本的。seq并不关注这些分流函数的结果,因此能够同步去作一些操做,尤为是I/O操做。注意,它的特性是fork,后面的组合子中,将会基于fork进行扩展。

图片描述

汇集(converge)

/**
 * 函数签名: ((x1, x2, …) → z) → [((a, b, …) → x1), ((a, b, …) → x2), …] → (a → b → … → z)
 * 函数做用: 将输入值fork到各个forker函数中运行,并将结果集汇集到join函数中运行,返回最终结果
 * 函数特性: fork-join
 * @param {Function} join 汇集函数
 * @param {...Function} forkers 分捡函数列表
 * @returns join函数的返回值
 */

// ES5
function converge (join, forkers) {
  return function (val) {
    var args = [];
    for (var i = 0; i < forkers.length; i++) {
      args[i] = forkers[i](val);
    }
    join.apply(null, args);
  }
}

// ES6+
const converge = (join, forkers) => val => join(...forkers.map(forker => forker(val)));

// eg: 数组求平均数
var len = arr => arr.length;
var sum = arr => arr.reduce((init, item) => init + item, 0);
var div = (sum, len) => sum / len; 
var avg = converge(div, [sum, len]);
avg([1,2,3,4,5]); // => 3

图片描述

图片描述

converge函数实际上是seq的祖先,你看他们的特性都是fork,但为何说convergeseq的先祖呢?学了那么多组合子的知识,咱们来尝试用converge来重写seq吧:

// 不用组合子的写法
const seq = (...fns) => converge(nothing, fns);
// 使用组合子的写法
const seq = spread(curry(converge)(nothing));
const seq = spread(partial(converge, [nothing]));

重写的思路也很简单,首先使用彻底函数应用,而后找参数的特色,不使用组合子(即彻底函数应用)的时候,咱们发现seq形参converge的第二实参是对应的,但coverge的第二实参是一维的,所以须要用spread升维。而后converge的第一实参前置出来便可,因此咱们可使用curry或者partial。看,组合子再一次发挥了极其重要的做用。

映射(map)

/**
 * 函数签名: (a → b) → [a] → [b]
 * 函数做用: 将系列输入值映射到`transfomer`函数中运行,并将结果整理成新的系列
 * 函数特性: map
 * @param {Function} transfomer - 转换器
 * @returns 通过映射后的新系列
 */

// ES5
function map (transfomer) {
  return function (arr) {
    var result = [];
    for (var i = 0; i < arr.length; i++) {
      result.push(transfomer(arr[i]));
    }
    return result;
  }
}

// ES6+
const map = transfomer => arr => arr.map(transfomer);

图片描述

map竟然是组合子,不少人一脸茫然。是的,你没听错,在ES5上增长的这些数组函数,都是组合子,只不过它是数组实例的方法。但推的更广一点,但凡具有Iterator特性的对象,均可以具有map方法。而在范畴论中,Functor也是能够具有map方法的,这不在咱们本章的讨论范围呢,咱们假定拥有map特性的都是集合。以上代码中,咱们用本身的方式抽离出了map函数,它的特色是,将集合中的每一项提取并映射到目标函数transfomer中,并将结果从新整理成集合。map操做和fork操做都很是的类似,前者是数组分发,后者是单值分发。因而可知,mapfork特性(不是函数)是能够相互转换的,感兴趣的同窗能够继续往下看。

图片描述

前面咱们提到过unary组合子,可是没有给出实现方式以及实际用途,如今咱们能够结合map组合子来使用。首先是unary的代码形式:

/**
 * 函数签名: (* → b) → (a → b)
 * 函数做用: 将二元以上的函数转换成一元的(不推荐转零元)
 * 函数特性: 降维
 * @param {Function} fn - 原函数
 * @returns 降为一元的新函数
 */

// ES5
function unary (fn) {
  return function (value) {
    return fn(value);
  }
}

// ES6+
const unary = fn => value => fn(value);

而后咱们来看以下的案例:

// 假设数据从某个文件中获取,转换出来以后是一个字符串数组
const datasFromFile = ['1', '2', '3', '4'];
// 对字符串数组进行转换,转变为数字数组
datasFromFile.map(parseInt); // => [1, NaN, NaN, NaN]

为何会出现[1, NaN, NaN, NaN]这样的结果?若是你对parseInt函数了解,应该知道它有两个参数string(被解析的字符串)和radix(解析基数)。第二个参数告诉程序string会以什么样的进制数进行解析,这个函数咱们很少赘述了,只要知道默认值是0就能按照十进制进行解析。出现这个问题的缘由是由于Array.prototype.map组合子中的transfomer默认带有三个参数:item(项)、index(项索引)、array(数组实例)。所以调用时,索引被添加上去致使后面的字符串没法按照该索引所确立的进制数进行转换,但使用了unary就能够:

datasFromFile.map(unary(parentInt)); // => [1, 2, 3, 4]

分捡(useWith)

/**
 * 函数签名: ((x1, x2, …) → z) → [(a → x1), (b → x2), …] → (a → b → … → z)
 * 函数做用: 将系列输入值映射到各个transfomer函数中运行,并将结果集汇集到join函数中运行,返回最终结果
 * 函数特性: map-join
 * @param {Function} join - 汇集函数
 * @param {Function[]} transfomers - 转换器
 * @returns join函数的返回值
 */

// ES5
function useWith (join, transfomers) {
  return function (vals) {
    var args = [];
    for (var i = 0; i < transfomers.length; i++) {
      args[i] = transfomers[i](vals[i]);
    }
    join.apply(null, args);
  }
}

// ES6+
const useWith = (join, transfomers) => vals => join(...transfomers.map((transfomer, i) => transfomer(vals[i])));

// eg:
var square = val => Math.pow(val, 2);
var sumSqrt = (a, b) => Math.sqrt(a + b);
var pythagoreanTriple = useWith(sumSqrt, [square, square]);
pythagoreanTriple([3, 4]); // => 5
pythagoreanTriple([5, 12]); // => 13
pythagoreanTriple([7, 24]); // => 25

图片描述

useWith函数和converge函数很是的类似,咱们从它们的结构图上能够发现,一个是先map,一个是先fork。这两个函数在实际使用中很常见的。

规约(reduce)

/**
 * 函数签名: ((a, b) → a) → a → [b] → a
 * 函数做用: 将初始值代入`reducer`的第一参数,输入系列映射为`reducer`的第二参数,并将`reducer`的返回值迭代到下次`reducer`的第一参数中,将最终返回值构成新的系列
 * 函数特性: reduce
 * @param {Function} reducer 规约函数
 * @param {*} init 初始数
 */

// ES5
function reduce (reducer, init) {
  return function (arr) {
    var result = init;
    for (var i = 0; i < arr.length; i++) {
      result = reducer(result, arr[i]);
    }
    return result;
  }
}

// ES6+
const reduce = (reducer, init) => arr => arr.reduce(reducer, init);

图片描述

reduce是集合中一个比较特殊的函数,功能特性为“折叠”,可以将一个列表折叠成一个单一输出。用来作统计是很是不错的。它常和其它函数联系使用,不只能实现功能,还能让代码的语意化变得有艺术感,这里很少作赘述。和reduce很像的组合子还有sortfilterflat等,它们的特别之处就是要等待谓词函数(一种契函数)的嵌入,才能发挥真正的做用。这些函数的特性既不是改变维度,也不是控制逻辑流程(参数分发和结果选择),它们是真正具有数据处理功能的函数。

组合(compose)

/**
 * 函数签名: ((y → z), (x → y), …, (o → p), ((a, b, …, n) → o)) → ((a, b, …, n) → z)
 * 函数做用: 将输入值代入最末函数,并将结果代入上一个函数,直到全部函数所有调用完成,返回最终结果
 * 函数特性: chain
 * @param {...Function} fns - 函数列表
 * @returns 从下到上依次执行的结果
 */

// ES5
function compose() {
  var fns = [].slice.call(arguments);
  var len = fns.length;
  return function (val) {
    var result = val;
    for (var i = len - 1; i >= 0; i--) {
      result = fns[i](result);
    }
    return result;
  }
}

// ES6+
const compose = (...fns) => val => fns.reverse().reduce((result, fn) => fn(result), val);

如今,咱们抽离出更加通用的组合函数compose,能够将任意个函数组合在一块儿。但注意,除了最后一个函数能够是可变元的,其它的函数都应该是一元的,它的执行顺序是从后到前,若是不适应这样的方式,也能够reverse一下参数,构成命令行中常见的管道方式pipe函数:

const pipe = reverse(compose);

谓语组合子

谓词,用来 描述断定客体性质、特征或客体之间关系的词项。

谓词函数,用于表达是什么(is)作什么(do)怎么样(how)等的函数。

谓语组合子是一种最多见的函数组合子,它须要组合谓词函数(predicate)(或叫断言函数)来实现其功能,这个咱们前面已经接触过一次。常见的关键字有ofbyiswhendo等等,在实际开发中咱们已经见过不少谓语组合子,只是你们都不知道它们的称呼。下面,咱们来从新回顾一下。

过滤(filter)

例如,有限数字列表或哈希中,过滤出偶数。此时是偶数isEven就是谓词函数

// 构建`是什么`的`谓词函数`
const isEven = n => n % 2 === 0;
// 将`isEven`嵌入到`filter`组合子中
const filter = fn => list => list.filter(fn);
const getEvens = filter(isEven);
getEvens([1, 2, 3, 4]); // => [2, 4]

谓词函数返回boolean类型,嵌入filter后发生效用。与filter具有一样特性的组合子有不少,例如:findeverysome等。

分组(group)

例如,将一个对象数组按照对象的name字段进行分组。此时name字段byName就是谓词函数

// 构建`怎么样`的`谓词函数`
const byName = obj => obj.name;
// 将`byName`嵌入到`group`组合子中
const group = fn => list => list.reduce((groups, item) => {
  const name = JSON.stringify(fn(item));
  groups[name] = groups[name] || [];
  groups[name].push(item);
  return groups;
}, {});
const groupByName = group(byName);
groupByName([
  {name: 'A', tag: 'a'},
  {name: 'B', tag: 'b'},
  {name: 'A', tag: 'α'},
  {name: 'B', tag: 'β'}
]);

谓词函数返回某个属性,嵌入group后发生效用。与group具有相同特性的组合子有不少,例如:flatpair等。

排序(sort)

sort组合子须要嵌入一个comparator函数,是一个比较函数,用于描述两个参数之间作比较(即作什么)的过程。咱们先来看看最简单的例子:

const diff = (a, b) => a - b;
const sort = fn => list => list.sort(fn);
const asc = sort(diff);
asc([4, 2, 7, 5]); // => [2, 4, 5, 7]

谓词函数返回一个单值,嵌入sort后发生效用。与sort具有相同特性的组合子有不少,例如mapreduce等。

其它

这里,咱们再次讲到了reduce,与它类似的,map也是谓语组合子,它们主要负责组合作什么这类的谓词函数。因而可知,在函数组合子这一节中提到的大部分组合子都是具有谓语特性的,主要目的是达成谓语的"作什么":

const add = (a, b) => a + b;
const sum = list => list.reduce(add, 0);
sum([1, 2, 3, 4]); // 10

const pow = (x, n) => Math.pow(x, n); // x的n次方
const squ = list => list.map(pow);
squ([2, 2, 2, 2]); // [1, 2, 4, 8]

谓语组合子还有不少不少,其目的就是将某种功能的可开放性交给谓词函数进行扩展。通常在函数库中,见到相似以下字眼并后跟函数参数的,颇有可能就是谓语组合子tobywhilewhenwithofall/everyany/somenone…… 像iseqgt用于判断的谓词函数,有的是用于谓语组合子的,有的是由函数组合子构造出来的。

一些常见函数库中的谓语组合子

// lodash
_.countBy
_.dropRightWhile
_.differenceWith
// ramda
R.indexBy
R.takeWhile
R.mergeWith

组合子变换

回顾一下咱们已经掌握的组合子,它们具有的特性以下:

  1. 变换维度(升维、降维、换序、偏应用、柯里化);
  2. 数据流程(id、or、fork、map、join);
  3. 数据处理(reduce、sort、filter...)

咱们不只认识了这些组合子,而且知道他们是经过什么方法得到的,也尝试了用已经学过的组合子来构建起他等效的组合子。如今咱们手动构建其它要想或者可能会用到的组合子:

juxt

juxt将函数列表做用于值列表,在没有封装以前,咱们看它是怎么使用的:

// 获取一系列数的范围
const getRange = juxt([Math.min, Math.max]);
getRange(3, 4, 9, -3); // => [-3, 9]

这个方法使用上和以前的组合子很有一些类似,到底是哪些地方类似,只要找出来,谜题天然解开。这个函数的特性是:

  1. 参数为一系列函数,即函数数组;
  2. 最终输入为一系列输入;
  3. 全部的输入都是同时参与这一系列函数的处理;

很显然,第三点就是咱们以前学过的fork-join特性。并且参数和最终输入都很是的类似,有区别的是converge的最终输入是一元的,且它有一个join函数。因而咱们能够知道,要想改造converge成为juxt,须要:

  1. 降维,将参数进行压缩后再展开;
  2. join函数用id消除便可;
var juxt = fns => spread(converge(spread(identity), fns.map(gather)));

如今,你经过juxt构建的函数来构建getRange而且代入数据,结果彻底一致,其数据代入的过程是:

  1. (3, 4, 9, -3)通过外部spread的逆运算变为[3, 4, 9, -3]
  2. 每一个函数都被map内的gather函数处理过,所以[3, 4, 9, -3]都会通过内部的gather的逆运算变为(3, 4, 9, -3)
  3. Math.minMath.max可执行参数为(3, 4, 9, -3)的运算,获得结果(-3, 9)
  4. (-3, 9)通过内部spread的逆运算,变为[-3, 9]
  5. [-3, 9]代入到identity函数,返回原值[-3, 9]

如今是否是以为特别的神奇,有兴趣的朋友能够尝试进行其它组合。

实战案例

数据判断

咱们有这样的对象信息从sessionStorage中获取(该代码摘至芒果项目):

{
  authorites: ['xxxx', 'xxxx', 'ROLE_ADMIN', 'xxxx', 'xxxx']
}

这个对象描述了一个用户信息的缓存,其中authorites反应了用户所具有的权限,若是权限字段中带有ROLE_ADMIN,则咱们认为他就是一个系统管理员。若是不使用函数式编程,会是这样的:

// 不使用函数式(ES6+)
function isAdmin (userInfo) {
  var authorites = [];
  if (userInfo.hasOwnProperty('authorites')) {
    authorites = userInfo.authorites || [];
  }
  return authorites.some(item => item === 'ROLE_ADMIN');
}

而后咱们看看函数式编程会怎么的改写,这里咱们将用到比较出名的函数式函数库Ramda

// 使用Ramda(ES6+)
const isAdmin = R.pipe(            // 1. 从上到下串联(组合)函数
    R.prop('authorites'),            // 2. 获取`数据`的`authorites`信息
  R.defaultTo([]),                    // 3. 数据处理,若是为`undefined`、`null`或`NaN`则返回`[]`
  R.contains('ROLE_ADMIN')    // 4. 判断是否包含`ROLE_ADMIN`信息
);

这段代码明显就比没有使用函数式的代码要剪短很多,这不足为奇,你甚至还能发现userInfo这个参数没有了。这段代码应该从上往下读,由于咱们使用了能组合函数的pipe函数:

  1. R.pipe将各个函数依次从上往下执行的串联(组合)起来;
  2. R.prop用于获取上一个输入的对象的authorites属性;
  3. R.defaultTo用于设置一个默认值;
  4. R.contains用于判断数组中是否含有ROLE_ADMIN这一项;

R.pipe将各个函数串联(组合)起来,并返回一个新的函数,这个新函数的输入就是userInfo,它不是不存在,而是被Pointfree化,中文翻译为“无参风格”。这个代码若是完整的写则表示为:

const isAdmin = userInfo => R.pipe(...)(userInfo);

函数式中有一个重要特性就是:若是f = x => g(x),那么f === g。这就是Pointfree风格。它不是彻底无参,只是弱化了数据自己的形式,而注重过程(方法)的实现。数据进去以后会获取一个authorites信息a,然而处理该信息的默认值b,最后判断是否包含预约信息c,并将结果c返回。因为isAdmin = R.pipe(f1, f2, f3),经过f1/f2/f3就能计算出isAdmin,那么整个过程就根本不须要知道a/b/c,甚至连最开始的数据均可以不须要知道。咱们把数据处理的过程,从新定义成了一种与参数无关的合成(pipecompose)运算,这种将数据进行更加抽象的方式使得函数变得可自由组合,从而提高复用性。但这也要求,咱们在编写函数时,参数应该更加偏向抽象的数据形式,而尽量不要偏向业务。后面的例子,咱们也会用到Pointfree风格,并讲到使用无参风格所须要的一些条件。

数据转换

如今,咱们尝试作以下两种数据之间的转换:

var list = [{id: 1, name: 'a'}, {id: 2, name: 'b'}, /* ... */];
var obj = {
  "1": {id: 1, name: 'a'},
  "2": {id: 2, name: 'b'},
  /* ... */
};

这是一个很是常见的列表转哈希需求,目的是为了给列表数据作缓存,如今咱们不用函数式来实现:

// 不用函数式(ES6+)
const list2object = function (list) {
  const result = {};
  list.forEach(item => {
    result[item.id] = item;
  });
  return result;
};

若是咱们查阅Ramda文档,很容易将该函数进行改写,但咱们先不这么作,咱们来看看这样的函数有什么 问题?不难发现,取id这一操做应该是能够配置的。咱们只须要加入谓词函数便可:

// 使用Ramda(ES6+)
const list2objectBy = name => R.compose(
  R.indexBy,        // 2. 根据谓词函数进行索引转换(转换器)
    R.prop(name)    // 1. 实现的谓词函数,按照属性名进行转换(转换规则)
);
const list2objectById = list2objectBy('id');
const list2objectByName = list2objectBy('name');

这样,list2objectById能够将id提取成list2objectByName能够将name提取成list2objectBy成为了创造函数的函数,咱们很是巧妙且灵活的运用了函数式的灵活性。

Mendix案例——MxObject对象数据提取

刚才的案例,都是比较小比较弱的案例,如今来让咱们看更大的案例。这是出如今Mendix前端组件开发时,发现的一个问题,首先咱们先描述一下环境,让你们对这个有个基本认识:

  1. Mendix Client 经过特定api可获取订阅数据MxObject;
  2. MxObject是一个超级大对象,可经过一系列api获取对象属性;
  3. 第二条所说的属性就是数据库中的数据;

不过很惋惜的是,MxObject只能经过get方法获取某一个属性,不能直接得到整个对象的JSON值。若是咱们但愿经过console.log来打印一个MxObject对象,那就很繁琐了,要一个个属性去转,若是订阅获取到的数据是列表MxObjects,会更麻烦。好在该对象的JSON存根中有这样的一段信息,形如:

{
  jsonData: {
    attributes: {
      customAttr: {value: '@value'}
    },
    guid: '@guid'
  }
}

一个完整的表达是这样的:

{
  jsonData: {
    attributes: {
      name: {value: 'bill'}
      age: {value: 45},
        address: {value: 'usa'}
        /* ... */
    },
      guid: '9876543210'
  }
}

咱们发现这段数据存根很是的诡异,它有以下特征:

  1. 全部的数据字段都存储在jsonData中;
  2. id信息存在guid中,其它信息存在attributes中;
  3. id信息的值是直接存储的,其它信息的值是存储在value键值对中的;

如今,咱们须要 将它转换成这样的格式:

{ 
  id: '9876543210',
    name: 'bill',
  age: 45,
  address: 'usa',
  /* ... */
}

也能够简化成以下的形式:

{
  guid: '@guid',
  customAttr: '@value'
}

若是不使用函数式,相信各位都会很轻松的写出来,但咱们讲的是函数式,并且我使用的是Ramda库,因此我是这样去处理的:

// 因为id和其它属性存储方式不同,所以咱们要分开处理

// 1. 先处理id,处理的思路就是`对象提取`
const getJSONFromMxObjectWithGuid = R.pipe(
  // 获取MxObject.jsonData属性
  R.prop('jsonData'),
  // 筛选出guid键值对
  R.pick(['guid'])
);
// 2. 再处理非id字段,处理的思路就是`遍历键值对`,再进行时`属性提取`
const getJsonFromMxObjectWidthoutGuid = R.pipe(
    // 获取MxObject.jsonData.attributes属性
  R.path(['jsonData', 'attributes']),
  // 遍历键值对,提取value属性构成新键值对
  // {customAttr: {value: 'value'}} => {customAttr: 'value'}
  R.map(R.prop('value'))
);
// 3. 合并数据
const getJsonFromMxObject = R.converge(
  R.merge, 
  [getJsonFromMxObjectWidthoutGuid, getJSONFromMxObjectWithGuid]
);

如今问题来了,咱们是指望将guid提取出来,变成id的,可是上面的代码中,咱们没有对提取的键值进行转换,咱们须要修改源代码吗?在函数式的帮助下,咱们的答案是不须要,因为Ramda并无更换对象键名的方法,因此咱们要本身手动建立一个:

/**
 * 重命名Object的键名
 * @curried 已柯里化
 * @param {String} oldKey -  旧键名
 * @param {String} newKey -  新键名
 */
const renameKey = R.curry((oldKey, newKey) => R.converge(
  // 3. 合并对象(合并1.*和2的操做)
  R.merge, [
    // 2. 删除旧键
    R.omit([oldKey]), 
    R.compose(
      // 1.2 建立新键值对对象
      R.objOf(newKey), 
      // 1.1 获取旧键值
      R.prop(oldKey)
    )
  ]
));

而后咱们新增一个方法,并修改最终的函数:

const getJSONFromMxObjectWithId = R.pipe(
  getJSONFromMxObjectWithGuid,
  renameKey('guid', 'id')
);
// 3. 合并数据
const getJsonFromMxObject = R.converge(
  R.merge, 
  [getJSONFromMxObjectWithId, getJSONFromMxObjectWithGuid]
);

总结

通过一系列的长文,稍显“粗略”的介绍了一下函数式编程中组合子的构成、特色和使用方式。之因此说是“粗略”的介绍,是由于有关组合子的内容还有更多更深的,在数学和计算机领域真实存在,但被JavaScript所实现并应用的确实很少,例如:

  1. A组合子apply);
  2. B组合子(已经讲到过的compose方法)
  3. K组合子constant
  4. Y组合子fix
  5. C组合子flip
  6. I组合子(已经讲到过的identity方法)
  7. S组合子substitution
  8. T组合子thrush
  9. P组合子psi

这些内容再讲深一点,就能够讲到函子(Functor)的概念了, 这超出了咱们须要掌握的范围。有兴趣的朋友能够参阅fantasy-landFantasy-Land-Specification 中文翻译),这里有一套已经实现大部分组合子的类库combinators-js。有机会的话,会给你们讲解比较浅显的函子概念。

认识一些经常使用的组合子后,咱们发现了组合子的妙用,也感觉到了函数式所带来的代码美化哲学。但这仅仅是函数式编程中很小的一块,但也是最为实用的。来回顾一下它的特性:

  1. 组合子是一种高阶函数;
  2. 组合子不改变原函数契函数)的原有功能特性;
  3. 组合子能够经过变换参数转变流程控制输出加强函数
  4. 组合子可经由其它组合子等效转换成其它组合子

而组合子的使用条件也比较苛刻:

  1. 不管是组合子函数仍是契函数都必须是绝对的;
  2. 组合子必须保证数据的不变性持久性

感谢如下原创系列文章给本文带来的认知提高和书写灵感:

准备充分了嘛就想学函数式编程 系列

跌宕起伏的函数式编程 系列

JS 函数式编程指南 系列

JavaScript 轻量级函数式编程 系列

Thinking in Ramda 系列

Ramda 杂谈 系列

函数式编程中的“函数们”

代数 JavaScript 规范

Transducers Explained: Part 1 中文

Transducers Explained: Pipelines 中文

JavaScript 中的 Currying(柯里化) 和 Partial Application(偏函数应用)

一步一步教你 JavaScript 函数式编程(第一部分)

一步一步教你 JavaScript 函数式编程(第二部分)

一步一步教你 JavaScript 函数式编程(第三部分)

JavaScript 函数式编程术语大全

函数式编程入门教程

JavaScript中的函数式编程(英文原文)

相关文章
相关标签/搜索