7、 函数式编程(纯函数的好处、柯里化使用和实现、代码组合compose、pipe)

函数式编程在做什么

一个简单的例子,海鸥程序

var Flock = function(n) {
  this.seagulls = n;
};
Flock.prototype.conjoin = function(other) {
  this.seagulls += other.seagulls;
  return this;
};
Flock.prototype.breed = function(other) {
  this.seagulls = this.seagulls * other.seagulls;
  return this;
};
var flock_a = new Flock(4);
var flock_b = new Flock(2);
var flock_c = new Flock(0);
var result = flock_a.conjoin(flock_c).breed(flock_b).conjoin(flock_a.breed(flock_b)).seagulls;
//=> 32  

var result = flock_a.conjoin(flock_c).breed(flock_b).conjoin(flock_a.breed(flock_b)).seagulls;
这行代码,我们来解析一下是干了什么操作(不看也行,知道比较难理解且数据可能错误就可以),

  1. flock_a.conjoin(flock_c).breed(flock_b).conjoin(flock_a.breed(flock_b)).seagulls;执行了flock_a.breed(flock_b),假设返回的结果是结果1(此时flock_a的seagulls值被改变了,从4变成了8)
  2. flock_a.conjoin(flock_c).breed(flock_b).conjoin(结果1).seagulls,此时要执行conjoin,在此之前要先得到flock_a.conjoin(flock_c).breed(flock_b)的值
  3. flock_a.conjoin(flock_c).breed(flock_b),要执行breed,在此之前要得到flock_a.conjoin(flock_c)的值。
  4. flock_a.conjoin(flock_c),执行flock_a.conjoin(flock_c),得到结果2=flock_a.conjoin(flock_c) (此时的flock_a已经在第一步被改变了)
  5. 反推回第三步,结果2.breed(flock_b),得到结果3=flock_a.conjoin(flock_c).breed(flock_b)
  6. 反推回第二步,结果3.conjoin(结果1).seagulls
  7. 最后的result=结果3.conjoin(结果1).seagulls
  8. 最后的result=32,不是我们使用这个函数期望得到的值

很复杂,我们用函数重新写一下

 var conjoin = function(flock_x, flock_y) { return flock_x + flock_y };
var breed = function(flock_x, flock_y) { return flock_x * flock_y };
var flock_a = 4;
var flock_b = 2;
var flock_c = 0;
var result = conjoin(breed(flock_b, conjoin(flock_a, flock_c)), breed(flock_a, flock_b));
//=>16

这次我们得到了正确的答案,而且少写了很多代码。不过函数嵌套有点让人费解…(我们会在代码组合解决这个问题)

代码中的两个函数除了函数名有些特殊,其他没有任何难以理解的地方。我们把它们重命名一下。

var add = function(a, b) { return a + b };
var multiply = function(a, b) { return a * b };
var x = 4;
var y = 2;
var z = 0;
var result = add(multiply(y, add(x, z)), multiply(x, y));
//实际上执行y*(x+z)+x*y=>(2x+z)*y=>multiply(add(2*x,z),y)
//=>16

比原来清晰非常多,也不会因为改变对象内部的原本值而造成语义错误,这就是函数化编程的优点。

纯函数

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用(不会影响别人)。
比如:slice 和 splice
slice不会改变原数组,splice会改变原数组

var xs = [1,2,3,4,5];
// 纯的
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]
// 不纯的
xs.splice(0,3);
//=> [1,2,3]
xs.splice(0,3);
//=> [4,5]
xs.splice(0,3);
//=> []

在函数式编程中,我们讨厌这种会改变数据的笨函数。我们追求的是那种可靠的,每次都能返回同样结果的函数,而不是像 splice 这样每次调用后都把数据弄得一团糟的函数,这不是我们想要的。

副作用可能包含,但不限于:

  1. 更改文件系统;
  2. 往数据库插入记录;
  3. 发送一个 http 请求;
  4. 可变数据;
  5. 打印/log;
  6. 获取用户输入;
  7. DOM 查询;
  8. 访问系统状态…

从定义上来说,纯函数必须要能够根据相同的输入返回相同的输出;如果函数需要跟外部事物打交道,那么就无法保证这一点了。

追求纯函数的原因

  • 可缓存性(Cacheable)
  • 可移植性/自文档化(Portable / Self-Documenting)
  • 可测试性(Testable)
  • 合理性(Reasonable)
  • 并行代码

可缓存性(Cacheable):

首先,纯函数总能够根据输入来做缓存。实现缓存的一种典型方式是 memoize 技术:

var squareNumber  = memoize(function(x){ return x*x; });
squareNumber(4);
//=> 16
squareNumber(4); // 从缓存中读取输入值为 4 的结果
//=> 16
squareNumber(5);
//=> 25
squareNumber(5); // 从缓存中读取输入值为 5 的结果
//=> 25

下面的代码是一个简单的实现,尽管它不太健壮。

var memoize = function(f) {
  var cache = {};
  return function() {
    var arg_str = JSON.stringify(arguments);
    cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
    return cache[arg_str];
  };
};

可移植性/自文档化(Portable / Self-Documenting):

纯函数是完全自给自足的,它需要的所有东西都能轻易获得。仔细思考思考这一点…这种自给自足的好处是什么呢?
首先,纯函数的依赖很明确,因此更易于观察和理解。

// 不纯的
var signUp = function(attrs) {
  var user = saveUser(attrs);
  welcomeUser(user);
};

var saveUser = function(attrs) {
    var user = Db.save(attrs);
    ...
};

var welcomeUser = function(user) {
    Email(user, ...);
    ...
};

// 纯的
var signUp = function(Db, Email, attrs) {
  return function() {
    var user = saveUser(Db, attrs);
    welcomeUser(Email, user);
  };
};

var saveUser = function(Db, attrs) {
    ...
};

var welcomeUser = function(Email, user) {
    ...
};

这个例子表明,纯函数对于其依赖必须要明确,这样我们就能知道它的目的

可测试性(Testable):

纯函数让测试更加容易。只需简单地给函数一个输入,然后断言输出就好了

合理性(Reasonable):

使用纯函数最大的好处是引用透明性,如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的。
由于纯函数总是能够根据相同的输入返回相同的输出,所以它们就能够保证总是返回同一个结果,这也就保证了引用透明性。

var isSameTeam = function(player1, player2) {
  return player1.team === player2.team;
};
 
 var punch = function(player, target) {
  if(isSameTeam(player, target)) {
    return target;
  } else {
    return target;
  }
};
var jobe ={name:"Jobe", hp:20, team: "red"};
var michael = {name:"Michael", hp:20, team: "green"};
punch(jobe, michael);

如上图案例:isSameTeampunch函数都是纯函数
首先punch代入isSameTeam

//isSameTeam(player, target)=> player.team === target.team
 var punch = function(player, target) {
  if(isSameTeam(player, target)) {
    return target;
  } else {
    return target;
  }
};

因为是不可变数据,我们可以直接把 team 替换为实际值:

var punch = function(player, target) {
  if("red" === "green") {
    return target;
  } else {
    return target;
  }
};

if 语句执行结果为 false,所以可以把整个 if 语句都删掉:

var punch = function(player, target) {
    return target;
};

等式推导带来的分析代码的能力对重构和理解代码非常重要。事实上,我们重构海鸥程序使用的正是这项技术:利用加和乘的特性。

并行代码:

最后一点,也是决定性的一点:我们可以并行运行任意纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会副作用而进入竞争态

柯里化

柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)©。
curry 的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
你可以一次性地调用 curry 函数,也可以每次只传一个参数分多次调用。先执行其中一部分,另一部分可以等待,在后面再执行。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

柯里化?目的是什么?

使用:

//match已经柯里化
match(/\s+/g, "hello world");
// [ ' ' ]

match(/\s+/g)("hello world");
// [ ' ' ]

var hasSpaces = match(/\s+/g);
// function(x) { return x.match(/\s+/g) }

hasSpaces("hello world");
// [ ' ' ]

hasSpaces("spaceless");
// null

可以看出,只需传给函数一些参数,就能得到一个新函数。

例如,我们有一个用于格式化和输出信息的日志(logging)函数 log(date, importance, message)。在实际项目中,此类函数具有很多有用的功能,例如通过网络发送日志(log),在这儿我们仅使用 alert:

function log(date, importance, message) {
  alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}

让我们将它柯里化!

log = _.curry(log);

柯里化之后,log 仍正常运行:

log(new Date(), "DEBUG", "some debug"); // log(a, b, c)

……但是也可以以柯里化形式运行:

log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)

现在,我们可以轻松地为当前日志创建便捷函数:

// logNow 会是带有固定第一个参数的日志的偏函数
let logNow = log(new Date());

// 使用它
logNow("INFO", "message"); // [HH:mm] INFO message

柯里化实现

function curry(func) {
// func 是要转换的函数
  return function curried(...args) {
    if (args.length >= func.length) {//如果传进来的参数大于等于func函数参数,则执行函数。
      return func.apply(this, args);
    } else {
      return function(...args2) {//否则进行返回一个函数,返回的函数会带上新传入的参数和原来的参数合并,执行curried 进入新一轮的参数长度判断(if判断)
        return curried.apply(this, args.concat(args2));
      }
    }
  };
}

柯里化 是一种转换,将 f(a,b,c) 转换为可以被以 f(a)(b)© 的形式进行调用
我们以f(a,b,c,d) =>f(a)(b,c)(d)为例,理解一下上述的柯里化实现
1、 f(a)执行,传入a,一个参数,小于f(a,b,c)的参数个数4
2、进入else分支,返回一个函数Fn,Fn的函数体是一个执行curried(a,新参数),f(a)执行完毕。返回Fn。
3、执行Fn(b,c),传入b,c,执行Fn的函数体,curried(a,b,c)
4、curried的if分支判断,参数个数3,小于4,继续返回一个函数Fn2(函数名字不重要,没有名字也可以),Fn2的函数体是一个执行curried(a,b,c,新参数)
5、执行Fn2(d), 执行Fn2的函数体,curried(a,b,c,d)
6、curried的if分支判断,参数个数4,等于4,执行f(a,b,c,d)

只允许确定参数长度的函数

柯里化要求函数具有固定数量的参数。

使用 rest 参数的函数,例如 f(…args),不能以这种方式进行柯里化。

代码组合

compose:从右到左依次执行函数
pipe:从左到右依次执行函数

使用:
实现功能:先将数据进行去除空格,然后转大写,最后转数组

const name = "   linlin   ";
// 去空格
const trim = (str) => str.trim();
// 转大写
const toUpper = (str) => str.toUpperCase();
// 转数组
const toArray = (str) => str.split("");

没使用事件组合时:result=toArray(toUpper(trim(name))) 三层函数嵌套
使用compose:result=compose(toArray, toUpper, trim)(name) 从右到左,执行trim(name), 将结果1返回给toUpper,执行toUpper(结果1),将结果2返回toArray,执行toArray(结果2),最后结果返回result
使用pipe:result=pipe(trim, toUpper, toArray)(name) 与compose只有顺序不同

实现:
compose:

function compose(...fns) {
  // 动态个数,因为我们不需要访问 fns 参数长度
  // 数组的遍历方法
  // map
  // reduce
  // reduceRight
  // forEach

  return function (x) {
    return fns.reduceRight((value, fn) => {
      return fn(value);
    }, x);
  };
}

pipe:

function pipe(...fns) {
  // 动态个数,因为我们不需要访问 fns 参数长度
  // 数组的遍历方法
  // map
  // reduce
  // reduceRight
  // forEach

  return function (x) {
    return fns.reduce((value, fn) => {
      return fn(value);
    }, x);
  };
}

reduce、reduceRight是数组的方法,第一个参数是前一个数据的执行结果,后续会有专门章节介绍数组方法。

  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值