# 范畴 | 容器 | 函子

# 范畴

“范畴”是一数学里的一个概念,它包含两个东西,一个是值,一个是值的变形关系(函数)。范畴论就是使用函数,表达范畴之间的关系。

范畴论的发展衍生出了一整套函数的运算方法,这套方法起初只用于数学运算,有人将它在计算机上实现,就变成了今天的“函数式编程”。

为什么函数式编程要求函数必须是纯的?因为它是一种数学运算,原始的目的就是求值,不做其它事情。

# 容器与函子

容器就是一个 Container,里面有 value 值。如果 Container 里如果有一个 map 方法,该方法将容器里面的每一个值,映射到另一个容器,那么这个 Container 就是一个函子。

函子是函数式编程中最重要的数据类型,也是基本的运算单位和功能单位。它是一种范畴,也是一个容器,包含了值和变形关系。特殊的是,它的变形关系可以依次作用于每一个值,将当前的容器变成另外一个容器。

var Container = function(x) {
  this._value = x;
};

//一般约定,函子有一个of方法
Container.of = (x) => new Container(x);

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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

函数式编程里的运算都是通过函子完成。函子本身具有对外接口(map 方法),各种函数就是运算符,通过接口介入容器,引发容器里面值的变形。

因此学习函数式编程就是学习函子的各种运算,运用不同的函子解决实际问题。

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

  static of(x) {
    return new Functor(x);
  }

  map(f) {
    return new Functor(f(this.val));
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 常见的函子

# Pointed 函子

Pointed 函子是实现了 of 静态方法的函子。of 方法是为了避免使用 new 来创建对象。

Functor.of = function(val) {
  return new Functor(val);
};
//js中的Array.of
Array.of("test"); //["test"];
1
2
3
4
5

# Maybe 函子

Maybe 用于处理错误和异常。函子接受各种函数,处理容器内部的值。内部的值可能是一个空值,而函数外部未必有处理空值的机制。如果传入空值,很可能就会出错。

var Maybe = function(x) {
  this._value = x;
};

Maybe.of = function(x) {
  return new Maybe(x);
};

Maybe.prototype.map = function(f) {
  return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this._value));
};

Maybe.prototype.isNothing = function() {
  return this._value === null || this._value === undefined;
};
//新的容器称之为Maybe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

ES6 的写法

Functor.of(null).map(function(s) {
  return s.toUpperCase();
});
//TypeError
class Maybe extends Functor {
  map(f) {
    return this._value ? Maybe.of(f(this._value)) : Maybe.of(null);
  }
}

Maybe.of(null).map(function(s) {
  return s.toUpperCase();
}); //Maybe(null)
1
2
3
4
5
6
7
8
9
10
11
12
13

Maybe 函子只能在执行的那次判断是否为 null,如果中间有多次 map,某一次又出现了 null,这个时候就处理不了了。这个时候就需要 Either 函子。

# Either 函子

Either 函子有两个作用,一个是实现 try/catch/throw, 主要用来做错误处理。try/catch/throw 并不是纯的,因为它从外部接管了我们的函数,并在函数出错时抛弃了它的返回值。

Either 函子还表示两者中的任意一个,类似 if...else 处理。

Either 函子内部有两个值:左值和右值。右值是正常情况下使用的值,

# 错误处理

var Left = function(x) {
  this._value = x;
};

var Right = function(x) {
  this._value = x;
};

Left.of = function(x) {
  return new Left(x);
};

Right.of = function(x) {
  return new Right(x);
};

//不同点

Left.prototype.map = function(f) {
  return this;
};

Right.prototype.map = function(f) {
  return Right.of(f(this._value));
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

ES6 写法:

class Left {
  static of(x) {
    return new Left(x);
  }
  constructor(x) {
    this._value = x;
  }
  map(fn) {
    return this;
  }
}

class Right {
  static of(x) {
    return new Right(x);
  }
  constructor(x) {
    this._value = x;
  }
  map(fn) {
    return Right.of(fn(this._value));
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Left 和 Right 的唯一区别就在与 map 方法的实现。Left.map 方法不会对容器做任何事情,只是简单的把容器拿进来又扔出去。这个特性使得 Left 可以用来传递一个错误消息。

例子:

var getAge = (user) => (user.age ? Right.of(user.age) : Left.of("error"));

getAge({ name: "xiaohong", age: "21" }).map((age) => "Age is " + age); //Right("Age is 21");
1
2
3

# 条件运算

class Either extends Functor {
  constructor(left, right) {
    this.left = left;
    this.right = right;
  }

  map(f) {
    return this.right ?
      Either.of(this.left, f(this.right)) :
      Either.of(f(this.left), this.right);
  }
}

Either.of = function (left, right) {
  return new Either(left, right);
};

var addOne = function (x) {
  return x + 1;
};

Either.of(5, 6).map(addOne);
// Either(5, 7);

Either.of(1, null).map(addOne);
// Either(2, null);

function parseJSON(json) {
  try {
    return Either.of(null, JSON.parse(json));
  } catch (e: Error) {
    return Either.of(e, null);
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# AP 函子

函子里面包含的值,可能是函数。AP 函子解决的就是函子里的 value 是函数的情况。 ap 是 applicative(应用)的缩写。凡是部署了 ap 方法的函子,就是 ap 函子

class Ap extends Functor {
  //  static of(x) {//ES6可以继承
  //      return new Ap(x);
  //  }
  //  constructor(x) {
  //      this._value = x;
  //  }
  ap(F) {
    return Ap.of(this.val(F.val));
  }
}

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

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

Ap.of(addTwo).ap(Functor.of(2));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

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

function add(x) {
  return function(y) {
    return x + y;
  };
}
Ap.of(add)
  .ap(Maybe.of(2))
  .ap(Maybe.of(3));
// Ap(5)
1
2
3
4
5
6
7
8
9

# Monad 函子

函子是一个容器,可以包含任何值。函子之中再包含一个函子,也是合法的,但这样会出现多层嵌套的函子。

Maybe.of(Maybe.of(Maybe.of({ name: "Mulburry", number: 8402 })));
1

上面这个函子,一共有三个 Maybe 嵌套。如果要取出内部的值,就要连续三次调用 this.val。这很不方便,于是出现了 Monad 函子。

Monad 函子的作用是,总是返回一个单层的函子。它有一个 flatMap 方法,与 map 方法的作用相同。唯一的区别就是如果生成了嵌套函子,它会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。

class Monad extends Functor {
  join() {
    return this.val;
  }
  flatMap(f) {
    return this.map(f).join();
  }
}
1
2
3
4
5
6
7
8

上面代码中,如果函数 f 返回的是一个函子,那么 this.map(f)就会生成一个嵌套的函子。所以,join 方法保证了 flatMap 方法总是返回一个单层的函子。这意味着嵌套的函子会被铺平(flatten)。

Monad 是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。只需要提供下一步运算所需的函数,整个运算就会自动执行下去。js 中的 Promise 就是一种 Monad。Monad 可以让我们避免了嵌套地狱,可以轻松处理深度嵌套的函数式编程,比如 IO 和其它异步任务。

Monad 函子的重要应用,就是实现 I/O 操作。

# IO

I/O 是不纯的操作,普通的函数式编程没法做。这时就需要把 IO 操作写成 Monad 函子,通过它来完成。

import _ from "lodash";
var compose = _.flowRight;

var IO = function(f) {
  this._value = f;
};

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

IO.prototype.map = function(f) {
  //把f组合之后,return 出去,让外部去执行,将不纯的函数变为纯的
  return new IO(compose(f, this._value));
};

//ES6 写法

class IO extends Monad {
  map(f) {
    return IO.of(compose(f, this._value));
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

举个例子:

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;
  });
};

readFile("./user.txt")
  .flatMap(tail)
  .flatMap(print);

//最后在Monad函子中执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

完整代码:

var fs = require("fs");
var _ = require("lodash");
//基础函子
class Functor {
  constructor(val) {
    this.val = val;
  }
  map(f) {
    return new Functor(f(this.val));
  }
}
//Monad 函子
class Monad extends Functor {
  join() {
    return this.val;
  }
  flatMap(f) {
    //1.f == 接受一个函数返回的是IO函子
    //2.this.val 等于上一步的脏操作
    //3.this.map(f) compose(f, this.val) 函数组合 需要手动执行
    //4.返回这个组合函数并执行 注意先后的顺序
    return this.map(f).join();
  }
}
var compose = _.flowRight;
//IO函子用来包裹📦脏操作
class IO extends Monad {
  //val是最初的脏操作
  static of(val) {
    return new IO(val);
  }
  map(f) {
    return IO.of(compose(f, this.val));
  }
}
var readFile = function(filename) {
  return IO.of(function() {
    return fs.readFileSync(filename, "utf-8");
  });
};
var print = function(x) {
  console.log("🍊");
  return IO.of(function() {
    console.log("🍎");
    return x + "函数式";
  });
};
var tail = function(x) {
  console.log(x);
  return IO.of(function() {
    return x + "【京程一灯】";
  });
};
const result = readFile("./user.txt")
  //flatMap 继续脏操作的链式调用
  // .flatMap(print);
  .flatMap(print)()
  .flatMap(tail)();
console.log(result.val());
// console.log(result().val());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

# 当下流行的函数式编程库

  • Rxjs 原理必会

  • lodash 原理必会

  • Underscore

  • Ramdajs

# 总结

  • 并发编程。函数式编程不用考虑死锁,因为它不修改变量。可以将工作分摊到多个线程,部署并发编程。

  • 单元测试。函数式编程可以方便单元测试。因为我们只需考虑参数,不用考虑函数的调用顺序。

函数式编程带来了更高的可组合型,灵活性以及容错性。现代 JS 库入 redux,都已经开始使用函数式编程。redux 的核心理念就是状态机和函数式编程。

最后更新时间: 1/22/2021, 8:08:33 PM