Partial & Curry - 函数式编程

什么是函数式编程

在文章以前,先和你们讲一下对于函数式编程(Functional Programming, aka. FP)的理解(下文我会用FP指代函数式编程):ajax

  1. FP须要保证函数都是纯净的,既不依赖外部的状态变量,也不产生反作用。基于此前提下,那么纯函数的组合与调用,在时间顺序上就不会产生依赖,改变多个函数的调用顺序也没必要担忧产生问题,所以也会消灭许多潜在的bug。
  2. 函数必须有输入输出。若是一个函数缺少输入或输出,那么它实际上是一段处理程序procedure而已。
  3. 函数尽量的保持功能的单一,若是一个函数作了多件事情,那么它理论上应当被拆分为多个函数。
  4. FP的意义之一就是,在适当的时机使用声明式编程,抽象了程序流的控制与表现,从理解和维护的角度上会胜于命令式编程。
  5. FP是一种范式,但并不意味这和OOP(面向对象编程)冲突,二者固然是能够和谐共存的。我的认为 React 其实就是一个很好的栗子~
  6. Javascript的函数一等公民以及闭包的特性,决定了Javascript的确是适合施展FP的舞台

理解闭包

闭包对于 Javascript 来讲,固然十分重要。然而对于函数式编程来讲,这更加是必不可少的,必须掌握的概念,闭包的定义以下:编程

Closure is when a function remembers and accesses variables from outside of its own scope, even when that function is executed in a different scope.api

相信大部分同窗都对闭包有不错的理解,可是因为对FP的学习十分重要。接下来我仍是会啰嗦的带你们过一遍。闭包就是可以读取其余函数内部变量的函数数组

简单示例以下闭包

// Closure demo
function cube(x) {
  let z = 1;
  return function larger(y) {
    return x * y * z++;
  };
}

const makeCube = cube(10);
console.log(makeCube(5)); // 50
console.log(makeCube(5)); // 100
复制代码

那么有没有想过在函数makeCube,或者也能够说是函数larger是怎么记住本来不属于本身做用域的变量x和z的呢?在控制台查看makeCube.prototype,点开会发现原来是有个[[Scopes]]这个内置属性里的Closure(cube)记住了函数larger返回时记住的变量x和z。若是多嵌套几层函数,也会发现多几个Closure(name)在[[Scopes]]Scopes[]数组里,按序查找变量。app

再看下图测试代码:ide

function cube(x) {
  return function wrapper(y) {
    let z = 1;
    return function larger() {
      return x * y * z++;
    };
  }
}

const makeCubeY = cube(10);
const makeCube = makeCubeY(5);
const $__VAR1__ = '1. This var is just for test.';
let $__VAR2__ = '2. This var is just for test.';
var $__VAR3__ = '3. This var is just for test.';
console.log(makeCubeY.prototype, makeCube.prototype);
console.log(makeCube()); // 50
console.log(makeCube()); // 100
复制代码

打印makeCubeY.prototype函数式编程

打印makeCube.prototype函数

经过这几个实验能够从另外一个角度去理解Javascript中闭包,一个闭包是怎么去查找不是本身做用域的变量呢?makeCube函数分别从[[Scopes]]中的Closure(wrapper)里找到变量y、z,Closure(cube)里找到变量x。至于全局let、const声明的变量放在了Script里,全局var声明的变量放在了Global里。工具

在学习FP前,理解闭包是尤其重要的~ 由于事实上大量的FP工具函数都使用了闭包这个特性。

工具函数

unary

const unary = fn => arg => fn(arg);
复制代码

一元函数,应用于当只想在某个函数上传递一个参数状况下使用。尝试考虑如下场景:

console.log(['1', '2', '3'].map(parseInt)); // [1, NaN, NaN]
console.log(['1', '2', '3'].map(unary(parseInt))); // [1, 2, 3]
复制代码

parseInt(string, radix)接收两个参数,而map函数中接收的回调函数callback(currentValue[, index[, array]]),第二个参数是index,此时若是parseInt的使用就是错误的。固然除了Array.prototype.map,大量内置的数组方法中的回调函数中都不止传递一个参数,若是存在适用的只须要第一个参数的场景,unary函数就发挥了它的价值,无需修改函数,优雅简洁地就接入了。(对于unary函数,fn就是闭包记忆的变量数据)

identity

const identity = v => v;
复制代码

有同窗会看到identity函数会以为莫名其妙?是干吗的?我第一眼看到也很迷惑?可是考虑如下场景:

console.log([false, 1, 2, 0, '5', true].filter( identity )); // [1, 2, "5", true]
console.log([false, 0].some( identity )); // false
console.log([-2, 1, '3'].every( identity )); // true
复制代码

怎么样?眼前一亮吧,没想到identity函数原来深藏不露,事实上虽然identity返回了原值,可是在这些函数中Javascript会对返回的值进行类型装换,变成了布尔值。好比filter函数。咱们能够看MDN定义filter描述以下(看标粗的那一句)。

filter() calls a provided callback function once for each element in an array, and constructs a new array of all the values for which callback returns a value that coerces to true.

constant

const constant = v => () => v;
复制代码

一样,这个函数...乍一看,也不知道具体有什么用。可是考虑场景以下:

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Hello!');
  }, 200);
});
p1.then(() => 'Hi').then(console.log); // Hi!
p1.then(constant('Hi')).then(console.log); // Hi!
p1.then('Hi').then(console.log); // Hello!
复制代码

因为Promise.prototype.then只接受函数,若是我仅仅只须要传递一个值时,那么constant便会提供这种便利。固然这个并无什么功能上的提高,可是的确提升了可阅读性,也是函数式编程的一个优势。

spreadArgs & gatherArgs

const spreadArgs = fn => argsArr => fn( ...argsArr );
const gatherArgs = fn => (...argsArr) => fn( argsArr );
复制代码

嗯这两个函数见名知义。分别用于展开一个函数的全部参数和收集一个函数全部参数,这两个函数明显对立,那么它们的应用场景又是什么呢?

spreadArgs函数示例以下:

function cube(x, y, z) {
  return x * y * z;
}

function make(fn, points) {
  return fn(points);
}

console.log(make(cube, [3, 4, 5])); // NaN
console.log(make(spreadArgs(cube), [3, 4, 5])); // 60
复制代码

gatherArgs函数示例以下:

function combineFirstTwo([v1, v2]) {
  return v1 + v2;
}

console.log([1, 2, 3, 4, 5].reduce(combineFirstTwo)); // Uncaught TypeError
console.log([1, 2, 3, 4, 5].reduce(gatherArgs(combineFirstTwo))); // 15
复制代码

看完以上代码,简单的两个工具函数,轻易的作到了对一个函数的转换,从而使其适用于另外一个场景。若是今后应该能够瞥见函数式编程的一点点魅力,那么下面的两个函数将给你们带来更多的惊喜。

partial & curry

const partial = (fn, ...presetArgs) => (...laterArgs) =>
  fn(...presetArgs, ...laterArgs);
  
const curry = (fn, arity = fn.length, nextCurried) =>
  (nextCurried = prevArgs => nextArg => {
    const args = [...prevArgs, nextArg];

    if (args.length >= arity) {
      return fn(...args);
    } else {
      return nextCurried(args);
    }
  })([]);
复制代码

相信你们对函数柯里化应该或多或少有点了解。维基百科定义:

在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,而且返回接受余下的参数并且返回结果的新函数的技术。

固然得益于闭包的强大威力,柯里化这个武器得以诞生于Javascript世界。请你们先精读以上关于partiel、curry函数的代码。

喝一杯咖啡~

先模拟一个ajax函数以下:

function ajax(url, params, callback) {
  setTimeout(() => {
    callback(
      `GET ${url} \nparams: ${params} \ndata: Hello! ${params} `
    );
  });
}
复制代码

考虑partial使用场景以下:

const fetchPerson = partial( ajax, "http://some.api/person" );

fetchPerson('Teddy Bear', console.log);
/* GET http://some.api/person params: Teddy Bear data: Hello! Teddy Bear */
复制代码

考虑curry使用场景以下:

const fetchPerson = curry(ajax)('http://some.api/person');
const fetchUncleBarney = fetchPerson('Uncle Barney');

fetchUncleBarney(console.log);
/* GET http://some.api/person params: Uncle Barney data: Hello! Uncle Barney */
复制代码

partial和curry函数功能类似,但又有具体的不一样应用场景,但整体来讲curry会比partial更自动化一点。

可是!相信看完示例的同窗又会有一连串问号?为何好好地参数不一次性传入,而非要分开屡次传入这么麻烦?缘由以下:

  1. 最首要的缘由是partial和curry函数都容许咱们经过参数控制将一个函数的调用在时间和空间上分开了。传统函数须要一次性将参数凑齐才能调用,可是有时候咱们能够提早预置部分参数,在最终须要触发此函数时,才将剩余参数传入。这时候partial和curry就会变得十分有用。
  2. partial和curry的存在让函数组合(compose)会更加便利。(函数组合也计划以后和你们分享,这里就不详细说了)。
  3. 固然最重要是也提高了可阅读性!一开始可能不这么觉得,可是若是你实践操做感觉以后,也许会改观。

P.S. 关于函数式编程的实践,你们可使用lodash/fp模块进行入门实践。

一些思考

由于我也是函数式编程的初学者,若有不正确的地方,欢迎你们纠正~

接下来仍是会继续整理FP的学习资料,学习实践,连载一些我对于函数式编程的学习与思考,但愿和你们一块儿进步~

谢谢你们(●´∀`●)~

相关文章
相关标签/搜索