简要介绍fp-ts(四)

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: "否" } }))
复制代码

这样的结果不能让人满意,gh并没有使用传入的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 > 22),而是将下限注入到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);
复制代码

这会让我们之前的努力都白费了。幸好Readerask方法可以帮助我们从DependencieslowerBound而不需要真的拿到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或者说(->) rmap就是函数的 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类型来说,这意味着任取三个字符串abc,都有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

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享