欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

js函数式编程总结

程序员文章站 2022-07-14 20:37:45
...

最近在看js的函数式编程,觉得很酷,此文是对一些知识的脉络梳理总结,主要是为了方便自己理解,并非原创知识。会在文末贴出参考文章链接。
文中主要提到10点,分别是:高阶函数、纯函数、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)的数学分支。
定义: 范畴就是使用箭头连接的物体。也就是说,彼此之间存在某种关系的概念、事物、对象等等,都构成"范畴"。随便什么东西,只要能找出它们之间的关系,就能定义一个"范畴"。
js函数式编程总结上图中,各个点与它们之间的箭头,就构成一个范畴。
箭头表示范畴成员之间的关系,正式的名称叫做"态射"(morphism)。范畴论认为,同一个范畴的所有成员,就是不同状态的"变形"(transformation)。通过"态射",一个成员可以变形成另一个成员。
七、函子(Functor):
我们可以把"范畴"想象成是一个容器,里面包含两样东西。
1、 值(value)
2、 值的变形关系,也就是函数。
函数不仅可以用于同一个范畴之中值的转换,还可以用于将一个范畴转成另一个范畴。这就涉及到了函子(Functor)。
函子首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。
js函数式编程总结上图中,左侧的圆圈就是一个函子,表示人名的范畴。外部传入函数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 数据。
js函数式编程总结现在要求是,找到用户 Scott 的所有未完成任务,并按到期日期升序排列。
js函数式编程总结过程式编程的代码如下:
js函数式编程总结上面代码不易读,出错的可能性很大。
现在使用 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 函数库参考教程