函子是范畴论中的个概念,它把一个范畴中的对象映成另一个范畴的对象,把一个范畴中的态射映成另一个范畴中的态射,形象理解为范畴之间的“映射”。
简单来说,函子是某种映射关系。函子Functor 在 JavaScript 里主要是将某些值或对象包含起来并实现对应的一些函数来处理这些数值或对象的函数,函子必须实现map函数,其在遍历对象值的时候返回一个新对象。
换句话说,函子是实现了map的对象。举个例子:
1 2 3 4 5 6 7 8 9
| const Box = function(val) { this.val = val; } Box.of = function(val) { return new Box(val); } Box.prototype.map = function(func) { return Box.of(func(this.val)); }
|
注意,以上实现中 of
的实现不是必要的,of
的实现可以让使用者不必使用 new
来新建对象,而是使用 .of()
来实现新建对象:
1 2 3 4 5 6 7 8
| let time = Box.of("2022-03-14");
time.map(i => i + ' 15').map(i => i + ':22')
|
实现了 of
的函子被称为 Pointed函子
。
在函数式编程中,函子一般用于错误处理!下面介绍几个常见的函子:
MayBe函子
MayBe函子以函数式的方式处理错误,提供检查 null
和 undefined
的能力。
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
| const MayBe = function(val) { this.val = val; } MayBe.of = function(val) { return new MayBe(val); } MayBe.prototype.isNull = function() { return this.val == null; } MayBe.prototype.map = function(func) { return this.isNull() ? MayBe.of(null) : MayBe.of(func(this.val)); }
let time = MayBe.of("2022-03-14"); .map(i => i + ' 15') .map(i => undefined) .map(i => i + ':22')
|
Option函子
Option函子 是 MayBe函子 进化版,MayBe函子并不能确定是在哪里值变为 null
的,而 Option函子 能够解决分支拓展问题。
Option函子主要解决正常和出错两种状态,所以需要两个分支,即需要两个函子。一种实现方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const Nothing = function(val) { this.val = val; } Nothing.of = function(val) { return new Nothing(val); } Nothing.prototype.map = function(func) { return this; }
const Some = function(val) { this.val = val; } Some.of = function(val) { return new Some(val); } Some.prototype.map = function(func) { return Some.of(func(this.val)); }
const Option = { Nothing: Nothing, Some: Some }
|
使用时可将其封装在函数内,使用 try ... catch
来捕获错误,如果出现错误就使用 Nothing
将错误信息包裹,如果无错误就用 Some
包裹。
1 2 3 4 5 6 7 8 9
| let ans; try { ans = Some.of(5); throw "a is less 5." } catch (e) { ans = Nothing.of(e) }
ans.map(i => i + 1).map(i => i % 2)
|
在 Rust 或 Java 等语言中也是有类似的功能。
Monad
Monad 实际上也是一个特殊的函子,它只是实现了 chain
的函子!
首先,给出Monad的实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const Monad = function(val) { this.val = val; } Monad.of = function(val) { return new Monad(val); } Monad.prototype.isNull = function() { return this.val == null; } Monad.prototype.map = function(func) { return this.isNull() ? Monad.of(null) : Monad.of(func(this.val)); }
Monad.prototype.join = function() { return this.isNull() ? Monad.of(null) : this.val; }
Monad.prototype.chain = function(func) { return this.map(func).join(); }
|
首先举个例子:有一个API请求,将其返回的结果用函子包裹:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| fetch("https://api.xygeng.cn/day").then(t => t.json()) .then(data => { let ans = Monad.of(data) .map(d => d.data) }) })
|
需求是将数据整合进一个数组,返回数组也使用Monad包裹:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| fetch("https://api.xygeng.cn/day").then(t => t.json()) .then(data => { let ans = Monad.of(data) .map(d => d.data) .map(d => { console.log(d) let arr = []; for (p in d) { arr.push([p, d[p]]); } return Monad.of(arr); }) console.log(ans) }) })
|
此时,返回的结果会有嵌套的 Monad,这时候需要借助 join
函数来展开。
1 2 3
| Monad.prototype.join = function() { return this.isNull() ? Monad.of(null) : this.val; }
|
使用方法为在 map
后面加个 join
: map().join()
。而 chian
实际上就是该写法的简略版!
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| fetch("https://api.xygeng.cn/day").then(t => t.json()) .then(data => { let ans = Monad.of(data) .map(d => d.data) .chain(d => { let arr = []; for (p in d) { arr.push([p, d[p]]); } return Monad.of(arr); }) }) })
|
所以,Monad
只是实现了 chain
的一个特殊函子!