类型类Monoid
Fp-ts中的Monoid
类型类用于抽象如何结合两个值。
它内部只有两个成员:
interface Monoid<A> {
empty: A;
concat: (x: A, y: A) => A;
}
concat
封装了结合两个值的逻辑,empty
是一个用于表示同样类型空值的值。我们可以写一个Monoid<number>
的实例来展示一下用加法结合两个数字。
const addition: Monoid<number> = {
empty: 0,
concat: (a, b) => a + b
}
addition.concat(4, 8) // 12
我们还能用这个做些什么呢?让我们研究一下另外一个fp-ts中很有用的方法foldMap
。
假设你有一组苹果,并且你有办法把两个苹果结合成一个苹果。有了这两样东西,你应该可以把这组苹果“压缩”成一个苹果:只需要把每个苹果都和它前边那个苹果结合起来。
再假设你有一组香蕉,并且你有办法把两个橘子组合成一个橘子。那么你还需要什么才能做到:把一组香蕉组合成一个橘子?如果你有办法可以把香蕉变成橘子,那么你只要把香蕉迭代一遍,把每一个香蕉都变成橘子,然后再通过橘子结合大法把它们变成一个橘子。
这就是foldMap
背后的运作模式。而且foldMap
是具有普适性的,这意味着它可以作用于一组任何东西上,A
和任何变成B
的方法(Monoid<B>
)。
下边是foldMap
函数的签名:
foldMap: <B>(M: Monoid<B>) => <A>(f: (a: A) => B) => (fa: A[]) => B
这个函数可以切分成三个部分理解:
<B>(M: Monoid<B>)
,返回是<A>(f: (a: A) => B)
,这个函数的返回值则是(fa: A[]) => B
。
总的来说,这个函数允许我们传入一个类型为A
的数组,一个类型为B
的monoid,还有一个函数可以把A
转换B
,并且最后返回一个单独的B
。说起来很拗口,我们来试着用这个函数来统计一下所有用户的登陆次数。
import { Monoid } from "fp-ts/lib/Monoid";
import { foldMap } from "fp-ts/lib/Array";
type User = {name: string, logins: number}
const users: User[] = [
{ name: "Paul", logins: 10 },
{ name: "Sue", logins: 7 },
{ name: "Bob", logins: 8 }
];
foldMap(addition)((u: User) => u.logins)(users); // 25
foldMap
允许我们把值通过Monoid
中的”转换“进行”转换“。如果这个列表里没有元素,foldMap
就直接使用Monoid
中的zero
值。
这个类似lodash中的sumBy
:
_.sumBy(users, u => u.logins)
不同之处在于我们可以用任何Monoid去组合这些值,而不是只能用加法Monoid。如果你想要计算这些列表里的用户哪些是频繁使用的用户要怎么办?如果一个用户登陆至少5次我们就认为他是频繁用户,因此我们需要把数组中的元素压缩一下。
首先,我们把数组从用户对象映射为boolean
(小于5次返回false
,否则是true
),然后通过用一个操作boolean值的monoid来结合这些boolean
值:
declare const andMonoid: Monoid<Boolean>;
const userIsFrequent = (u: User) => u.logins > 4;
foldMap(andMonoid)(userIsFrequent)(users); // true
andMonoid
的实现:
const andMonoid: Monoid<Boolean> = {
empty: true,
concat: (a, b) => a && b
}
那么foldMap
内部实现是如何呢?
const myFoldMap: <B>(M: Monoid<B>) => <A>(f: (a: A) => B) => (as: A[]) => B =
M => fab => as =>
as.reduce((b, a) => {
return M.concat(b, fab(a))
}, M.empty)
Last modified on 2021-02-20