函数式编程

基本概念

函数式编程,即使用函数的方式进行编程。这个函数是指数学领域的函数。
数学领域的函数本质上是一种对应关系:

y = x + 1

有x值,便可得到y值。于是这个函数本身表达了x→y的对应关系。函数式编程就是这样的思想:所有的函数都体现了一种映射。
常规的命令式编程是思维方式的直观表达。例如,要将一棵二叉树镜像反转:

var invertTree(root) {
	if(root != undefined) {
		var leftNew = invertTree(root.left)
		var rightNew = invertTree(root.right)
		root.left = rightNew
		root.right = leftNew
	}
	return root
}

首先判断当前节点是否为空,然后翻转左子树,再翻转右子树。最后将左右子树交换。
这就是命令式编程,直观地将所有步骤描述出来。最终的结果可能更改原始输入,也可能不会。
而对于函数式编程,则变成了:

var invertTree(root) {
	if(root != undefined) {
		return new Tree(root.value, invert(root.right), inver(root.left))
	}
	return root
}

直接定义一个映射关系,将原二叉树直接映射为一棵新二叉树。最终的结果一定不会改变原始输入。

函数式编程的方式

一个数学函数,入参确定,返回值就确定,函数无任何其他副作用。这就是个纯函数
在函数式编程中,每个封装的模块都是一个纯函数。于是通过对这些小功能模块的组合,就可以组合出各种大的功能模块,且每个大的功能模块也是个纯函数。
例如,我们定义了两个纯函数:f(x)和g(x)。已知x初值为1,为了求最终结果y,需要如下操作:

var t = f(1)
var y = g(t)

现在将f(x)和g(x)组合为一个纯函数来用:

var t = g(f(x))
var y = t(1)

于是两个简单的映射组合为一个复杂的映射。然而对于同一个输入,输出依然是不变的。
然而上述这种组合方式有个条件,那就是前一个函数的返回,刚好是后一个函数的输入。返回值只有一个,因此每个函数都只能有一个输入。而实际应用中,一个函数所需的参数往往不止一个。于是,我们需要某种方式,将多参函数转换为单参函数。这种转换方式称为柯里化。
因此,函数式编程有两个基本的运算:
1. 柯里化:将一个多参函数,转化为单参函数。
2. 合成:将多个纯函数组合为一个纯函数。且满足结合律。

结合律是指改变结合的函数对象,只要执行顺序不变,最终结果就不变。
例如,有3个纯函数:f(x),g(x),h(x)。运算要求这3个纯函数顺序执行。于是:
(f(x)·g(x))·h(x) == f(x)·(g(x)·h(x)) == (f(x)·g(x)·h(x))
即:将f(x)和g(x)组合为一个纯函数,然后再与h(x)组合,等价于将f(x),g(x),h(x)这3个直接组合为一个纯函数。

柯里化

考虑这样一个例子:输入法定年龄s与指定年龄x。判断x>=s。
现在,给定s=18,x1=16,x2=20。
常规写法:

var isAdult = function(s, x) {
	return (x>=s)
}
isAdult(18, 16) // false
isAdult(18, 20) // true

然而这种写法每次都需要将标准值18传入。采用柯里化的写法:

var isAdult = function(s) {
  return function(x) {
    return (x>=s)
  }
}
var isAdultS = isAdult(18)
isAdultS(16) // false
isAdultS(20) // true

将其改为lambda表达式写法:

var isAdult = s => (x => (x>=s))
var isAdultS = isAdult(18)
isAdultS(16) // false
isAdultS(20) // true

上面的例子体现了柯里化的两个能力:
1. 将一个单次调用的多参函数,转换为多次调用的单参函数。
2. 将前面已传入的参数保存起来。

组合

有3个操作:x+1x*2x*3。现在有x=1,要执行两个计算:

  1. 先执行r = x+1,再对结果r执行r*2。
  2. 先执行r = x+1,再对结果r执行r*3。

常规实现思路:

var add1 = x => (x + 1)
var mul2 = x => (x * 2)
var mul3 = x => (x * 3)
mul2(add1(1)) // 4
mul3(add1(1)) // 6

功能上没有问题。然而调用的时候是两个函数顺序调用,是命令式的写法。将函数进行组合:

var add1 = x => (x + 1)
var mul2 = x => (x * 2)
var mul3 = x => (x * 3)
var a1m2 = x => mul2(add1(x)) // 组合
var a1m3 = x => mul3(add1(x)) // 组合
a1m2(1) // 4
a1m3(1) // 6

直接调用组合后的函数即可得到结果。
组合有如下特点:

  • 从右向左调用。上面例子中是先调用add1()再调用mul2()
  • 先调用函数的返回值会作为后调用函数的参数。
  • 所有函数都是单参数。

组合通常定义一个功能函数compose()compose的定义为

var compose = (...args) => x => args.reduceRight((value, item) => item(value), x);

使用compose(),则上面的例子就可以改为:

var add1 = x => (x + 1)
var mul2 = x => (x * 2)
var a1m2 = compose(mul2, add1)
a1m2(1)// 4

注意compose()的参数顺序,其参数中的函数是从右向左执行的。

函子

将f(x)和g(x)进行组合,得到新的映射函数t(x)。
调用这个新的映射函数,所传入的数据一定是某种确定的类型,例如,传入的x是一个自定义的MyType类型。
于是考虑,定义一个对象,用于管理MyType数组。该对象可以方便地生成一个MyType数据,并添加到自身的数组中;也可以对自身管理的所有MyType数据依次调用映射函数t(x)。同时,还可以满足MyType数据的特定约束行为(例如尝试获取越界的数组值时,该对象就返回一个new出的MyType对象)。
这个管理对象,就是函子。
函子是一个具有map方法的容器。具体的函子容器对象内保存了原始数据值,它可以对容器内的所有值调用指定的纯函数,从而将这些值全部映射为处理后的值,并放入一个新的函子容器对象中。
例如,js的Array就是一个函子。一个Array对象,可以使用map来对所有值进行映射,映射后的值会构成一个新的Array对象返回。
实际上,任何具有map()方法的数据结构,都可以当做函子的实现。因此约定,函子的标志就是容器具有map()方法。
同时,函数式编程还约定,函子有一个of()方法,用于生成新的容器。
依然以Array为例:

  1. 生成一个新的容器:
var r = Array.of(1,2,3) // r = [1,2,3]
  1. 对新的容器使用纯函数进行映射:
var Add1 = function(x) { return x + 1 }
var mult2 = function(x) { return x * 2 }
var r = r.map(Add1).map(mult2) // r = [2,6,8]

最后得到的r依然是个Array容器对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值