Monad与Cats杂谈
引子
这个引子有点长。一开始是log4j出了安全问题,有人想要重新写一个自己的。然后对于用什么格式作为配置文件引起了大讨论。 讨论过程中发现现在的格式还真不少,什么yaml啊,什么toml啊,什么hucon啊,以及其他杂七杂八知名的不知名的。
在这过程中我发现toml的文件格式定义似乎挺简单,遂想写一个解析器(不是parser generator,是parser combinator)。
看了看最近正好有个新的cats-parse
包,也是cats生态中的,于是用起来了。过程中自然少不了和functor、monad、traverse
之类的打交道,于是便有了此文。
注意:此文仅仅是想对范畴论相关的内容进行怯魅化,更准确的定义与描述请参考以下内容:
- 阮一峰的图解Monad
- Category Theory for Programmers
- Cats文档
- ncatlab(类似于百科)(本文写作的时候网站升级中,暂时无法访问)
范畴论其实不恐怖
我以前一直觉得Monad很恐怖,是个什么高大上的东西。为了学习它还去看了 Category Theory for Programmers学习范畴论 (不过最后也没看完就是了)。网上似乎也普遍认为很难,但其实Monad之类的概念也不是什么很恐怖的东西。
范畴论范畴论,就是讨论对象的分类以及分类之间的关系。 例如整数是个范畴,自然数负数都在其中;多边形是个范畴,三角形四边形五边形都在其中。 Monad也是个范畴,符合一定条件了就归入这个范畴。
以范畴论中最常见的functor函子为例,它的Cats中的定义如下:
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
直观的感觉来说它只是表示一类具备map
这个能力的对象。具体来说这类对象是个容器,并且具有把容器内的一类对象映射到另一个类型中的能力。
常见的具备这种能力的有:List
、Option
等。不具备这种能力的有很多,比如说JSON的列表。
虽然我可以把一个JSON列表中的每一个对象都换成另一个JSON对象,但它始终在JSON这一范畴之内,而不能变成其他的范畴,
因此JSON的列表不是一个函子。
而其他的Applicative、Monad等也是各自定义了一些条件,符合条件的对象了便进入了这个范畴。
以上的描述非常不严谨,不过只是希望给这些个概念进行怯魅。
Monad等很有用
这些常见的概念(Applicative、Monad等)各自定义了一些条件并因此具备一些能力。因此当你看到了一个对象属于某个范畴内, 你就知道这个对象满足这个范畴所定义的条件,因此也具备一些能力。而之所以它们会常用当然是因为我们经常会需要这些能力。
幺半群Monoid
一个简单的例子,当你看到一个幺半群(Monoid),你就知道它所对应的元素定义了二元运算, 并且该二元运算符合结合律,有一个单位元元素使得任何元素和这个单位元元素结合均为自身。
符合条件的最常见的就是定义在整数上的加法。元素:整数。结合律:a+(b+c) = (a+b)+c。单位元0:a+0 = 0+a = a
还有字符串结合。元素:字符串。结合律:a+(b+c) = (a+b)+c。单位元空字符串:a+"" = “"+a = a
那么它有什么用呢?当我们想将一堆元素通过这个运算减少到单个元素的时候(比如,列表求和),我们可以放心地分割这些元素, 并运用并行能力同时进行。因为我们知道对于1+2+3+4,我们可以1+2和3+4同时算再结合起来而不影响结果。
半环
可以参考我以前玩的项目。
函子Functor和幺元Monad
举例来说,我有一个解析器val p: Parser[Int]。它在尝试解析一定字符后会给我字符对应的数字以及剩下的字符串。 我想用这个数字除100,比如
100 / x`,但是我又担心它可能是0,那么我用如下函数:
def divideBy(x: Int): util.Try[Int] = util.Try(100 / x)
我知道Parser是个函子,因此我可以通过map
来替换它返回的东西:val p2: Parser[Try[Int]]: p.map(divideBy(_))
。
但是这个解析器之后还会和其他的解析器组合,例如距离时,解析完数字后还要解析长度单位。但我希望如果出错它就停下。 单单函子是不够用了,因为函子只能替换容器内部的东西,而我需要替换整个容器。根据当前结果生成这样的一个解析器很简单:
def judge(t: util.Try[Int]) = t.fold(error => Parser.failWith("除以零了"), v => Parser.pure(v))
根据之前运行的结果它会返回一个立即停下的解析器或者返回之前结果的解析器。但怎样让它与当前的解析器融合呢?
这里就要多亏了解析器是一个幺元,因为它具备flatMap
这个功能,可以让我合并新旧容器。
具体关于Monad的解说以及其他常见的范畴可以查看最开始给出的参考资料。
xxT是好帮手
当使用cats这个库的时候经常会看到xxT
的形式的类,比如EitherT
和OptionT
。它们本质上是好帮手,
帮忙解决一些常见的嵌套问题。MT[F, X ...]
就意味着F[M[X ...]]
,例如EitherT[Eval, String, Int]
就是Eval[Either[String, Int]]
。
它们提供了很多常见的函数。例如如果我想构造一个EitherT[Eval, String, Int]]
,我很可能需要这么写:
EitherT(Eval.now(Right(5)))
但是有了EitherT
,我们可以稍微快一点:
EitherT.rightT(5)
这个例子似乎没太大威力,不如换一个。
举例来说我有一个Eval[Option[Int]]
,我想给里面的数字加一,那我得这么写:
val v: Eval[Option[Int]] = Eval.now(Some(5))
v.map(_.map(_ + 1))
而如果它是一个OptionT[Eval, Int]
,我就可以这么写:
val v: OptionT[Eval, Int] = OptionT.some(5)
v.map(_ + 1)
看起来就干净很多了。同样的,拉平这层Option,可以这么写:
val v: Eval[Option[Int]] = Eval.now(Some(5))
v.map(_.getOrElse(3))
也可以这么写:
val v: OptionT[Eval, Int] = OptionT.some(5)
v.getOrElse(3)
总结
这篇杂谈稍微聊了聊范畴论以及scala的cats实现中的一些内容。希望能够有所帮助。