Reader
在 fp-ts 中,Reader
的定义如下:
interface Reader<R, A> {
(r: R): A
}
复制代码
也就是一个类型为r -> a
的函数,r
可以看作为计算所需的环境,而a
是计算的结果。它经常被用来做依赖注入。先来看一段代码:
const f = (b: boolean): string => b ? 'true' : 'false';
const g = (n: number): string => f(n > 2);
const h = (s: string): string => g(s.length + 1)
console.log(h('abc')) // 'true'
复制代码
这是三个普通的函数,没有什么特别之处。现在假设,我们想让f
更加国际化,比如来自中国的用户访问的时候,f
应当返回“是”和“否”。于是,我们为f
增加了一个参数:
interface Dependencies {
i18n: {
true: string,
false: string,
}
}
const f = (b: boolean, deps: Dependencies): string => b ? deps.i18n.true : deps.i18n.false;
复制代码
然而,这就导致了g
编译不通过,因为g
在调用f
的之后没有传入deps
。于是,我们必须为g
增加一个参数:
const g = (n: number, deps: Dependencies): string => f(n > 2, deps);
复制代码
同样的,这会导致h
的编译失败,然后我们需要为h
也增加一个参数:
const h = (s: string, deps: Dependencies): string => g(s.length + 1, deps)
复制代码
之后,我们终于可以给h
传入deps
,得到想要的结果了:
console.log(h("356", { i18n: { true: "是", false: "否" } }))
复制代码
这样的结果不能让人满意,g
和h
并没有使用传入的deps
,但是为了能够使用f
,他们必须拿到deps
。
那么有没有更好的解决方法呢?我们先来尝试一下如果将Dependencies
这个参数移出成为独立的一个参数。
const f =
(b: boolean): ((deps: Dependencies) => string) =>
(deps) =>
b ? deps.i18n.true : deps.i18n.false;
const g =
(n: number): ((deps: Dependencies) => string) => f(n > 2);
const h =
(s: string): ((deps: Dependencies) => string) => g(s.length + 1);
复制代码
而(deps: Dependencies) => string
就是Reader<Dependencies, string>
,所以我们也可以这样写:
const f =
(b: boolean): Reader<Dependencies, string> =>
(deps) =>
b ? deps.i18n.true : deps.i18n.false;
const g = (n: number): Reader<Dependencies, string> => f(n > 2);
const h = (s: string): Reader<Dependencies, string> => g(s.length + 1);
复制代码
假如我们不想 hard code 我们的长度下限(也就是,n > 2
的2
),而是将下限注入到g
中。首先,我们为Dependencies
增加一项:
interface Dependencies {
i18n: {
true: string,
false: string,
};
lowerBound: number;
}
复制代码
之后,我们想要从Dependencies
读到lowerBound
,但是我们不想写出这样的代码:
const g = (n: number): Reader<Dependencies, string> => (deps) => f(n > deps.lowerBound)(deps);
复制代码
这会让我们之前的努力都白费了。幸好Reader
的ask
方法可以帮助我们从Dependencies
读lowerBound
而不需要真的拿到deps
:
const g = (n: number): Reader<Dependencies, string> => pipe(
ask<Dependencies>(),
chain(deps => f(n > deps.lowerBound))
)
复制代码
ask
的实现如下:
const ask: <R>() => Reader<R, R> = () => identity
复制代码
我们来推导一下g
的返回值类型是不是符合Reader<Dependencies, string>
。首先ask()
返回Reader<Dependencies, Dependencies>
。chain
接受的参数类型是Dependencies -> Reader<Dependencies, string>
,返回类型确实是Reader<Dependencies, string>
。
关于Reader
最有趣的一点是,它和Either
一样 kind 为* -> * -> *
,也就意味着我们不能为 Reader
而只能为Reader r
创建一个 Functor
的 instance。对应的map
函数类型是:
map :: (a -> b) -> Reader r a -> Reader r b
复制代码
前面提到了Reader r a
也就是r -> a
,那么上述的类型其实就等价于:
(a -> b) -> (r -> a) -> (r -> b)
复制代码
对于compose
函数的定义:
compose :: (b -> c) -> (a -> b) -> (a -> c)
compose g f = \x -> g(f(x))
复制代码
就会发现原来Reader r
或者说(->) r
的map
就是函数的 composition。
Writer
fp-ts
中的Writer
定义如下:
interface Writer<W, A> {
(): [A, W]
}
复制代码
也就是一个返回 tuple 的函数。假如我们有这样一个函数:
const isHeavy = (weight: number) => weight > 80;
复制代码
这个函数只返回了一个布尔值,但是我们希望这个返回值能多告诉我们一些额外的信息,为此,我们对返回值进行修改:
const isHeavy =
(weight: number): W.Writer<string, boolean> =>
() =>
[weight > 80, "与80kg进行比较。"];
复制代码
这样,我们就知道了更多的上下文,而不是一个干巴巴的布尔值。但是这也会带来一个问题,假如我们还有一个函数如下:
const getWeight =
(user: User): W.Writer<string, number> =>
() =>
[user.weight, "取体重。"];
复制代码
getWeight
的返回值不能直接传给isHeavy
,因为isHeavy
需要的是一个普通的 number
,而getWeight
返回的是一个附带着上下文的值。不过,chain
能够在遵从上下文的前提下进行计算,所以我们再次用到它。这不过和之前不同的是,fp-ts的Writer
并没有直接提供Monad
的instance,我们需要使用getMonad
自己创建:
import * as W from "fp-ts/Writer";
const monad = W.getMonad(monoidString);
复制代码
getMonad
需求一个Monoid
的instance,这是因为Writer
接受的第一个类型并不一定是string
,也有可能是Array string
等。唯一的限制是它们需要能够提供Monoid
的instance。那么Monoid
这个 typeclass 又意味着什么呢。大家可能对 Monoid
这个英文名可能不太熟悉,但是很可能听过它的中文译名幺半群。
幺半群,是指在抽象代数此一数学分支中,幺半群是指一个带有可结合二元运算和单位元的代数结构(摘自百度百科。)
简单来说,幺半群就是具有单位元的半群。在编程的语境下:
interface Semigroup<A> {
readonly concat: (x: A, y: A) => A
}
interface Monoid<A> extends Semigroup<A> {
readonly empty: A
}
复制代码
也就是说,我们需要为A
定义一个符合结合律的二元运算concat
。对于string
类型来说,这意味着任取三个字符串a
、b
、c
,都有concat(concat(a, b), c) = concat(a, concat(b, c))
。操作符+
满足我们的要求,写成函数:
const concat: (a: string, b:string) => string = (a, b) => a + b;
复制代码
单位元意味着需要找到一个字符串e
,使得任取一个字符串a
都有concat(e, a) = concat(a, e)
。e
可以选择空字符串。所以三元组<string, +, ''>
就构成了一个半幺群。其他半幺群的例子有<number, +, 0>
、<boolean, &&, true>
等。
在通过getMonad
之后,我们就拿到了一个Monad
的 instance。于是我们就可以使用chain
做有带着上下文的运算了:
// 使用 execute 获得累积的额外信息
W.execute(chain(getWeight({ weight: 75 }), isHeavy)) // 取体重。与80kg进行比较。
// 使用 evaluate 取得计算结果
W.evaluate(chain(getWeight({ weight: 81 }), isHeavy)) // true
复制代码
State
在 fp-ts 中,State
的定义如下:
interface State<S, A> {
(s: S): [A, S]
}
复制代码
因为纯函数是无法访问外部的 state 的,因此在函数式编程中 state 需要以参数的形式传进来,然后函数不仅要返回根据 state 计算出来的结果,还需要返回新的 state。一个伪随机数生成器(pseudo random number generators,PRNG)的例子:the state monad。