js函数式编程基础要点总结

最近在看js的函数式编程,觉得很酷,此文是对一些知识的脉络梳理总结,主要是为了方便自己理解,并非原创知识。会在文末贴出参考文章链接。
文中主要提到11点,分别是:高阶函数、纯函数、PointFree、函数合成、函数柯里化、范畴、函子(Functor)、Either、Applicative Functor、IO、Monad。最后会列举一个示例。
一、高阶函数:
函数式编程的基础都会用到高阶函数,高阶函数是一个接收函数作为参数或将函数作为输出返回的函数。
例如:Array.prototype.map

let arr = [1, 2, 3]
let fun = (item) => {
  return item * 2
}
let result = arr.map(fun)
console.log(result) // => [ 2, 4, 6 ]

在本例中,fun就被当作参数传递给了mapmap就是一个高阶函数。
二、纯函数:
定义: 纯函数的定义是,对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。
副作用: 只要是跟函数外部环境发生的交互就都是副作用。
好处:
1、可缓存性,纯函数总能够根据输入来做缓存。
2、可移植性/自文档化,纯函数与环境无关、是完全自给自足的,它需要的所有东西都能轻易获得。
3、可测试性,无需伪造测试环境。
4、引用透明性,如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的。由于纯函数总是能够根据相同的输入返回相同的输出,所以它们就能够保证总是返回同一个结果,这也就保证了引用透明性。
三、PointFree:
PointFree写在前面,是因为觉得PointFree也是一种应该事先知道的基本思想。
定义:PointFree是一种思想,函数无须提及将要操作的数据是什么样的,不必声明参数,只在意运算过程。这样会让函数更简洁,但也并不是说一定要去除所有的参数。
例如:

//这不Piont free,这个str除了让代码变长,其实是毫无意义的。
var f = str => str.toUpperCase().split(' ');

在下面代码中fun函数是Piont free的, 但是为了保证funPiont free的,免不了让toUpperCasesplit 不那么Piont free,需要自行取舍。

var toUpperCase = word => word.toUpperCase();
var split = x => (str => str.split(x));
// compose是一种函数式方法,后面会提到,暂时先不用管。
var fun = compose(split(' '), toUpperCase);
f("abcd efgh");
// =>["ABCD", "EFGH"]

四、函数合成:
定义: 如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做"函数的合成"(compose)。
例如:

//两个函数的组合
var compose = function(f, g) {
    return function(x) {
        return f(g(x));
    };
};

//或者
var compose = (f, g) => (x => f(g(x)));

var add1 = x => x + 1;
var mul5 = x => x * 5;

compose(mul5, add1)(2);  // =>15 

五、函数柯里化:
在上面组合的示例中,有一个隐藏的前提,就是f和g都只能接受一个参数。如果可以接受多个参数,比如f(x, y)g(a, b, c),函数合成就非常麻烦。 这时就需要函数柯里化了。
柯里化: 把一个多参数的函数,转化为单参数函数。

//比较容易读懂的ES5写法
var add = function(x){
    return function(y){
        return x + y
    }
}

//ES6写法,也是比较正统的函数式写法
var add = x => (y => x + y);

//试试看
var add2 = add(2);
var add200 = add(200);

add(10)(20) // =>30
add2(2) // =>4
add200(50) // =>250

六、范畴:
函数式编程的起源,是一门叫做范畴论(Category Theory)的数学分支。
定义: 范畴就是使用箭头连接的物体。也就是说,彼此之间存在某种关系的概念、事物、对象等等,都构成"范畴"。随便什么东西,只要能找出它们之间的关系,就能定义一个"范畴"。
在这里插入图片描述上图中,各个点与它们之间的箭头,就构成一个范畴。
箭头表示范畴成员之间的关系,正式的名称叫做"态射"(morphism)。范畴论认为,同一个范畴的所有成员,就是不同状态的"变形"(transformation)。通过"态射",一个成员可以变形成另一个成员。
七、函子(Functor):
我们可以把"范畴"想象成是一个容器,里面包含两样东西。
1、 值(value)
2、 值的变形关系,也就是函数。
函数不仅可以用于同一个范畴之中值的转换,还可以用于将一个范畴转成另一个范畴。这就涉及到了函子(Functor)。
函子首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。
在这里插入图片描述上图中,左侧的圆圈就是一个函子,表示人名的范畴。外部传入函数f,会转成右边表示早餐的范畴。
可以自己写一个简单的容器:

var Container = function(x) {
  this.__value = x;
}
Container.of = x => new Container(x);

//试试看
Container.of(1);
//=> Container(1)

Container.of('abcd');
//=> Container('abcd')

我们调用 Container.of 把东西装进容器里之后,由于这一层外壳的阻挡,普通的函数就对他们不再起作用了,所以我们需要加一个接口来让外部的函数也能作用到容器里面的值:

Container.prototype.map = function(f){
  return Container.of(f(this.__value))
}

我们可以这样使用它:

Container.of(3)
    .map(x => x + 1)                //=> Container(4)
    .map(x => 'Result is ' + x);    //=> Container('Result is 4')

注意: 一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值,映射到另一个容器。
也就是说,如果我们要将普通函数应用到一个被容器包裹的值,那么我们首先需要定义一个叫 Functor 的数据类型,在这个数据类型中需要定义如何使用 map 来应用这个普通函数。
八、Either:
条件运算if…else是最常见的运算之一,函数式编程里面,使用 Either 函子表达。Left 和 Right 是它的两个子类。Either 函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的值,左值是在值不存在时使用。
用js实现一下:

class Either {
  constructor(value) {
    this._value = value
  }
  get value() {
    return this._value
  }
  static left(a) {
    return new Left(a)
  }
  static right(a) {
    return new Right(a)
  }
}
// 注意两个类的map方法不同
class Left extends Either {
  map() {
    return this
  }
}
class Right extends Either {
  map(f) {
    return Either.right(f(this.value))
  }
}

测试一下:

var str = '测试'
if (str) {
  return Either.right(str).map((x) => '有数据:' + x) // => Right { _value: '有数据:测试' }
}
return Either.left('没有数据') // => Left { _value: '没有数据' }

九、Applicative Functor:
函子里面包含的值,完全可能是函数。我们可以想象这样一种情况,一个函子的值是数值,另一个函子的值是函数。

function addTwo(x) {
  return x + 2;
}

const A = Functor.of(2);
const B = Functor.of(addTwo)

上面代码中,函子A内部的值是2,函子B内部的值是函数addTwo
有时,我们想让函子B内部的函数,可以使用函子A内部的值进行运算。这时就需要用到 ap 函子。
ap 是 applicative(应用)的缩写。凡是部署了ap方法的函子,就是 ap 函子。
用js实现一下:

var Ap = function (x) {
  this.val = x
}
Ap.of = function (val) {
  return new Ap(val)
}
Ap.prototype.map = function (f) {
  return new Ap(f(this.val))
}
// 注意:this.val 是一个函数,将会接收另一个 functor 作为参数,所以我们只需 map 它
Ap.prototype.ap = function (F) {
  return F.map(this.val)
}
function addTwo(x) {
  return x + 2
}

Ap.of(addTwo).ap(Ap.of(2)) //=> Ap { val: 4 }

ap 函子的意义在于,对于那些多参数的函数,经过柯里化,就可以从多个容器之中取值,实现函子的链式操作。

function add(x) {
  return function (y) {
    return x + y
  }
}

Ap.of(add).ap(Ap.of(2)).ap(Ap.of(8)) //=> Ap { val: 10 }

十、IO:
I/O 是不纯的操作,普通的函数式编程没法做,我们需要让不纯的操作变“纯”起来。我们采取的方式是定义一个IO的函子,然后将不纯的操作包裹起来,使其返回一个IO函子。
例如

var fs = require('fs');

var readFile = function(filename) {
  return new IO(function() {
    return fs.readFileSync(filename, 'utf-8');
  });
};

读取文件操作本来是不纯的,但是通过返回一个IO函子,使其变纯了。IO函子的定义如下:

const R = require('ramda');
// 注意,它的 __value 是一个函数
var IO = function (f) {
  this._value = f
}

IO.of = (x) => new IO(x)

IO.prototype.map = function (f) {
  return new IO(R.compose(f, this.__value))
}

测试一下:

var getUrl = new IO(() => window.location.href)

var result = getUrl.map(function (x) {
  return x
})
// 注意这里的取值方式,需要取到内层的_value()
console.log(result._value())

十一、Monad:
Monad 函子的作用是,总是返回一个单层的函子。
函子是一个容器,可以包含任何值。函子之中再包含一个函子,也是完全合法的。但是,这样就会出现多层嵌套的函子。

Maybe.of(
  Maybe.of(
    Maybe.of({name: 'Mulburry', number: 8402})
  )
)

上面这个函子,一共有三个Maybe嵌套。如果要取出内部的值,就要连续取三次this.val。这当然很不方便,因此就出现了 Monad 函子。
它有一个flatMap方法,与map方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。
下面用js实现以下,先定义一个join方法。

IO.prototype.join = function () {
  return this._value ? this._value : IO.of(null)
}

// 试试看
var foo = IO.of(IO.of('123'))
foo.join() //=> IO('123')

现在写另一个简化的IO操作:

const R = require('ramda')
var IO = function (f) {
  this._value = f
}
IO.of = (x) => new IO(x)
IO.prototype.map = function (f) {
  return new IO(R.compose(f, this._value))
}
IO.prototype.join = function () {
  return this._value ? this._value() : IO.of(null)
}

var map = R.curry((f, x) => x.map(f))
var join = (x) => x.join()

// 简化的获取数据的操作
var getData = function (x) {
  return new IO(() => {
    return x
  })
}
// 简化的对数据的处理操作1
var addStr1 = function (x) {
  return new IO(() => {
    return x + '测试1'
  })
}
// 简化的对数据的处理操作2
var addStr2 = function (x) {
  return new IO(() => {
    return x + '测试2'
  })
}
var test = R.compose(join, map(addStr2), join, map(addStr1), getData)
var reault = test('获取数据:')

console.log(reault._value()) // => 获取数据:测试1测试2

可以看到join 方法可以把Functor 拍平(flatten),我们不可能总是在 map 之后手动调用 join 来剥离多余的包装,现在写一个flatMap的方法。

IO.prototype.flatMap= function(f) {
  return this.map(f).join();
}
var flatMap = _.curry((f, functor) => functor.flatMap(f))

重新测试,依旧能行。

var test = R.compose(flatMap(addStr2), flatMap(addStr1), getData)
var reault = test('获取数据:')

console.log(reault._value()) // => 获取数据:测试1测试2

最后,由于我们返回的都是IO函子,所以可以实现方便的链式操作。像下面这样:

// var test = R.compose(flatMap(addStr2), flatMap(addStr1), getData)
var reault = getData('获取数据:').flatMap(addStr1).flatMap(addStr2)

console.log(reault._value()) // => 获取数据:测试1测试2

最后来看搬运的函数式示例吧,觉得对比清晰。
下面是一段服务器返回的 JSON 数据。
在这里插入图片描述现在要求是,找到用户 Scott 的所有未完成任务,并按到期日期升序排列。
在这里插入图片描述过程式编程的代码如下:
在这里插入图片描述上面代码不易读,出错的可能性很大。
现在使用 Pointfree 风格改写。


    var getIncompleteTaskSummaries = function(membername) {
      return fetchData()
        .then(R.prop('tasks'))
        .then(R.filter(R.propEq('username', membername)))
        .then(R.reject(R.propEq('complete', true)))
        .then(R.map(R.pick(['id', 'dueDate', 'title', 'priority'])))
        .then(R.sortBy(R.prop('dueDate')));
    };

另一种写法是,把各个then里面的函数合成起来。


    // 提取 tasks 属性
    var SelectTasks = R.prop('tasks');

    // 过滤出指定的用户
    var filterMember = member => R.filter(
      R.propEq('username', member)
    );

    // 排除已经完成的任务
    var excludeCompletedTasks = R.reject(R.propEq('complete', true));

    // 选取指定属性
    var selectFields = R.map(
      R.pick(['id', 'dueDate', 'title', 'priority'])
    );

    // 按照到期日期排序
    var sortByDueDate = R.sortBy(R.prop('dueDate'));

    // 合成函数
    var getIncompleteTaskSummaries = function(membername) {
      return fetchData().then(
        R.pipe(
          SelectTasks,
          filterMember(membername),
          excludeCompletedTasks,
          selectFields,
          sortByDueDate,
        )
      );
    };

最后,这篇文章也是自己在学习总结,主要为了自己理解,肯定有疏漏和问题。下面贴出一些参考原文,供大家查阅。

函数式编程入门教程
Pointfree 编程风格指南
函数式编程指北
JavaScript函数式编程
Ramda 函数库参考教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值