函数式编程与Ramda.js介绍

2,013 阅读5分钟

什么是函数式编程(Functional Programming)?

带着这个疑问搜了下。

函数式编程属于一种声明式的编程范式。

p1.jpg 而且对于Javascript这门语言

函数是一等“公民”

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

这个思想我们在React Hooks的设计上能够清楚的了解和认识它。

在函数式编程中,函数就是一个管道(pipe)。这头进去一个值(参数),那头就会返回一个计算后的值(返回值),没有其它副作用。而且只要参数一样,返回值也是一样的。

p2.jpg

一、基础概念

函数式编程其实是一种数学运算方法,当然后来也可以用来做一种编程范式的思想。它起源于一门叫做范畴论(Category Theory)的数学分支。

范畴论 这部分只简单了解部分概念即可,后面有部分概念的使用。

彼此之间存在某种关系的概念、事务等,都构成了“范畴”。对象(object)的搜集、态射的搜集、态射的组合都能形成一个“范畴”。 箭头表示范畴成员之间的关系,即“态射”(morphism)。 同一个范畴的所有成员,就是不同状态的变换(transformation)。

1643272455559-e5c9434f-5ae8-429d-9563-e0aad9c399ac.png

二、函数的合成和柯里化

函数式编程的两个最基本的运算

合成 柯里化

函数的合成

1643272439871-5aa50812-35ae-4050-b63a-1b33a458c026.png 如果一个值要经过多个函数处理,最后得到返回值,就可以把中间的函数合并成一个函数,这就叫“函数的合成”(compose)。

var compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
};

fg 都是函数,x 就是输入的参数,在它们之间通过“管道”传输的值。fg 组合的函数就是“函数的合成”。

函数的合成必须满足结合律

compose(f, compose(g, h))
compose(compose(f, g), h)
compose(f, g, h)

这个也就要求函数必须是“纯”的原因了。

pointfree模式:组合函数内部不需要提及将要操作的数据是什么

// 非 pointfree模式,因为提到了数据:name
var initials = function (name) {
  return name.split(' ').map(compose(toUpperCase, head)).join('. ');
};

// pointfree 模式
var initials = compose(
  join('. '), 
  map(compose(toUpperCase, head)), 
  split(' ')
);
initials("hunter stockton thompson"); // 'H. S. T'

柯里化(Curry)

f(x)g(x)合成为f(g(x)),有一个隐藏的前提是fg 都接受一个参数。如果要接受多个参数,就需要使用函数的柯里化: 将多参数的函数转化为单参数的函数。

// 柯里化之前
function add(x, y) {
  return x + y;
}
add(1, 2) // 3

// 柯里化之后
function addX(y) {
  return function (x) {
    return x + y;
  };
}
var addTen = addX(10)

addX(2)(1) // 3
addTen(1) // 11

三、函子

函数不仅可以在同一个范畴中的值转换,还可以将一个范畴转换为另一个范畴。这就用到了函子(functor)。

class Functor {
  constructor(val) { 
    this.val = val; 
  }

  map(f) {
    return new Functor(f(this.val));
  }
}

// const Functor = val => { this._val = val; }
// Functor.prototype.map = f => new Functor(f(this._val));

上面代码中,Functor就是一个函子。

一般约定,实现了Map方法的容器是一个函子的标志。

functor 就是一个签了合约的接口。我们本来可以简单地把它称为 Mappable

但是要生成一个这样的函子,需要先使用new实例化,这就不太好看了。 所以又约定函子有一个of方法,用来生成新的容器。

Functor.of = val => new Functor(val);

Maybe函子

函子里的map方法遇到nullundefined会报错, Maybe函子就是为了解决这个问题的

const Maybe = val => { this._val = val; }
Maybe.of = val => new Maybe(val);

Maybe.prototype.isNothing = () => this._val
Maybe.prototype.map = f => 
	this.isNothing() ? Maybe.of(f(this._val)) : Maybe.of(null);

Either 函子

Either 函子内部有两个值,左值(Left)和右值(Right),右值是正常情况的下使用的值,右值不正确时使用左值。

const Either = (left, right) => { 
  this.left = left;
  this.right = right;
}
Either.of = (left, right)  => new Either(left, right);

Either.prototype.map = f => this.right 
  ? Either.of(this.left, f(this.right)) 
  : Either.of(f(this.left), this.right);

Either 函子的常见用途是提供默认值

ap函子

ap函子的特点是一个函子的值是数值,一个函子的值是函数

Ap.prototype.map = F => Ap.of(this.val(F.val));

ap方法的参数不是函数,而是另一个函子 ap 函子的意义在于,对于那些多参数的函数,就可以从多个容器之中取值,实现函子的链式操作。

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

Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));

Monad函子

Monad 函子的重要应用,就是实现 I/O (输入输出)操作。

var fs = require('fs');

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

var print = function(x) {
  return new IO(function() {
    console.log(x);
    return x;
  });
}

var cat = compose(map(print), readFile);

cat(".git/config")
// IO(IO("[core]\nrepositoryformatversion = 0\n"))

从上可以看到IO嵌套一个IO,显得很奇怪,获取内部的值需要多次循环获取。 我们使用join方法将多层嵌套压成单层(flat),类似于多维数组转化为一维数组。

var ioio = IO.of(IO.of("pizza"));
// IO(IO("pizza"))

ioio.join()
// IO("pizza")

一个既定义了of方法又定义了join方法的算子,就叫Monad算子。Monad函子主要解决嵌套函子的取值问题。

Monad.prototype.join => this._val;
Monad.prototype.flatMap => this.map(f).join();

使用Monad函子进行IO操作变得更加简便(IO函子是一个Monad函子)

class IO extends Monad;

readFile('./user.txt')
.flatMap(print)

由于返回还是 IO 函子,所以可以实现链式操作。在大多数库里面,flatMap方法被改名成chain。

四、Ramda.js

和Underscore 和 Lodash 一样, Ramda也是JavaScript的一个工具库。和前两者最大的不同,Ramda专为函数编程风格而设计的,所有方法都支持柯里化。 柯里化

const addFour = (a, b, c, d) => a + b + c + d;

const curriedAddFour = R.curry(addFour);
const f = curriedAddFour(1, 2);
f(3)(4); //=> 10  等价于 curriedAddFour(1,2,3,4)或curriedAddFour(1)(2)(3)(4)

组合

const f = R.compose(Math.abs, R.add(1), R.multiply(2));

f(-4);  // 7

参考文档: