通过一些基础的函数式编程操作,我们得以更优雅地进行编程,但是之前使用的纯函数编程方法却并不能解决所有的问题。比如:控制流、异常处理、异步操作、状态变化等。这些问题都可以用本节抽象的概念解决。
Container
通过如下方式定义的对象称为一个简单的容器:
1
2
3
4
| class Container {
constructor(x) { this.$value = x; }
static of (x) { return new this.constructor(x); }
}
|
其中 static
函数 of
仅仅是为了方便代码更加函数化而定义,不影响理论探究的函数。
具体的,容器的操作应该遵循以下的约定:
Container
是一个有且仅有一个属性的对象,我们后面将抽象地将它命名为 $value
;$value
不能被约束为任何特定的类型,否则我们的使用场景将相当有限;$value
一旦进入容器,它将一只被设置在容器内。我们可以但不应当通过 .$value
这种访问属性的方式访问他。
Functor (Identity)
Functor
是一类特殊的 Container
,我们后续讨论的容器都是基于 Functor
的。它的简单实现:
1
2
3
4
5
| class Functor extends Container {
map(f) {
return Functor.of( f(this.$value) );
}
}
|
所以 Functor
是:一种实现了 map
方法的容器。
有了 Functor
,我们就可以像函数式编程一样处理有状态的问题,比如:
1
| Functor.of('bombs').map( append(' away') ).map( prop('length') ); // Functor.$value === 10
|
Functor
在 Container
中就像我们之前在 compose
中提到的恒等函数 id
一样平凡,所以通常也被称为 Identity
。
Maybe
上面的工作是简单平凡的,如果我们在 Container
中实现更多的方法,就可以拥有更丰富的功能。
Maybe
是一种用 Functor
实现空值检测的容器:
1
2
3
4
5
6
7
8
9
10
11
12
13
| class Maybe extends Functor {
get isNothing() {
return this.$value === null || this.$value === undefined;
}
map(fn) {
return this.isNothing ? this : Maybe.of( fn(this.$value) );
}
inspect() {
return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
}
}
|
通常情况下,我们强制要求每次调用都需要以 Maybe
作为返回值,但是如果我们非要用一个非 Maybe
的函数作为返回值,可以借助下面这种方式:
1
2
3
4
5
6
7
8
9
| // maybe :: b -> (a -> b) -> Maybe a -> b
var maybe = curry(function(x, f, m) {
return m.isNothing() ? x : f(m.$value);
});
// 返回值是 string, 而非 Maybe
var getTwenty = compose(
maybe("You're broke!", finishTransaction), withdraw(20)
);
|
Either
Either
也是一类特殊的 Functor
,它的本意是指返回值可以是一个 SumType。
这种 Fucntor
可以被用于进行错误处理,不同于 throw
/catch
,使用这种方式进行错误处理更加温和,我们可以定义一个左值作为发生异常时的类型(比如一个承载错误信息的字符串,类型为 String
),右值作为执行成功时的真正结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class Either extends Functor {
static of(x) {
return new Right(x);
}
}
class Left extends Either {
map(fn) { return this; }
}
class Right extends Either {
map(fn) {
try {
return Eitherr.of( fn(this.$value) );
} catch (e) {
return new Left(e);
}
}
}
|
IO
在基础部分提到进行函数编程的前提是需要纯函数,但是某些函数的执行结果由于依赖于外部的环境,所以相同的输入通常会得到不同的结果,我们可以这个函数原子化并以其本身作为返回值,构造一个二级函数,这个二级函数显然是一个纯函数。
以这个思想构造的容器称为 IO
,即 $value
为函数的 Functor
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| class IO extends Functor {
static of(x) {
return new IO(() => x);
}
constructor(fn) {
this.$value = fn;
}
map(fn) {
return new IO(compose(fn, this.$value));
}
inspect() {
return `IO(${inspect(this.$value)})`;
}
}
|
在系列函数执行之后,函数的 $value
本质是一个函数,我们还需要在最后执行它。但是这一直接访问 $value
的操作在我们的容器设计中总是不安全的,所以我们重命名为 unsafePerformIO
以指示这一区别。下面是一个应用 IO 的例子:
1
2
3
4
5
6
7
8
| const url = new IO(() => window.location.href);
const toPairs: string => string[][] = compose(map(split('=')), split('&'));
const params: string => string[][] = compose(toPairs, last, split('?'));
const findParam: string => IO<Maybe<string[]>> = key => url.map( compose(Maybe.of, find(compose(eq(key), head)), params) );
// impure code
findParam('searchTerm').unsafePerformIO();
|
PostScript:
map
函数的理解:第一个参数是映射函数,第二个参数是定义域,返回值则是值域。- 虽然我们现在定义的这些函数都是成员函数,但是之后它们可能会经常以独立函数的身份出现,它们通常被定义为“接受一个对应
Functor
类型并且执行对应成员函数”的函数,由于这个规定是为了优化 compose
函数的编写,因此容器类型的参数通常是最后一个参数。比如:map = curry( (fn, m) => m.map(fn) );
Task
最后异步类型的任务可以用一个叫 Task
的容器处理,这个容器的具体实现过程过于复杂,下面仅仅列举它的一个使用的例子:
1
2
3
4
5
6
7
8
9
| var getJSON: string => object => Task<Error, object> = curry(
(url, params) => new Task((reject, result) => {
$.getJSON(url, params, result).fail(reject);
})
);
var blogPage: object => HTML = Handlebars.compile(blogTemplate);
var renderPage: object => HTML = compose(blogPage, sortBy('date'));
var blog: object => Task<Error, object> = compose(map(renderPage), getJSON('/posts'));
|
实际函数在执行时,则需要调用 fork
函数进行异步地执行:
1
2
3
4
| blog({}).fork(
error => $('#error').html(error.message),
page => $('#main').html(page),
);
|
范畴学理论
上面提到的这些 Container
都被称为 Functor
,而 Functor 本身是可以构成一个范畴学意义上的 category
的:
- 恒等映射:
map(id) === id
; - 组合律:
compose(map(f), map(g)) === map(compose(f, g))
;