Scalaz:Cokleisli组合使用案例请求

17

这个问题并不是为了挑衅!正如你可能已经注意到的那样,我最近一直在研究Scalaz。我正在尝试理解为什么我需要库提供的某些功能。这里有一个例子:

import scalaz._
import Scalaz._
type NEL[A] = NonEmptyList[A]
val NEL = NonEmptyList

我在我的函数中加入了一些println语句以查看发生了什么(顺便说一下:如果我试图避免这样的副作用,我该怎么做呢?)。我的函数如下:

val f: NEL[Int] => String    = (l: NEL[Int]) => {println("f: " + l); l.toString |+| "X" }
val g: NEL[String] => BigInt = (l: NEL[String]) => {println("g: " + l);  BigInt(l.map(_.length).sum) }

然后我通过一个 cokleisli 将它们组合起来,并传入一个 NEL[Int]

val k = cokleisli(f) =>= cokleisli(g)
println("RES: "  + k( NEL(1, 2, 3) ))

这会打印什么?
f: NonEmptyList(1, 2, 3)
f: NonEmptyList(2, 3)
f: NonEmptyList(3)
g: NonEmptyList(NonEmptyList(1, 2, 3)X, NonEmptyList(2, 3)X, NonEmptyList(3)X)
RES: 57

RES值是最终NEL中(String)元素的字符数。我有两个想法:
  1. 我怎么知道我的NEL会从涉及的方法签名中以这种方式减少呢?(我根本没想到结果会是这样)
  2. 这有什么意义?可以为我概括一个相当简单易懂的用例吗?
这个问题实际上是请求像retronym这样可爱的人解释一下这个强大的库是如何工作的。
2个回答

18
为了理解结果,您需要了解Comonad[NonEmptyList]实例。Comonad[W]主要提供三个函数(在Scalaz中的实际接口略有不同,但这有助于解释):
map:    (A => B) => W[A] => W[B]
copure: W[A] => A
cojoin: W[A] => W[W[A]]

因此,Comonad 为某个容器 W 提供了接口,该容器具有特殊的“头”元素(copure),并公开容器的内部结构,使我们每个元素都得到一个容器(cojoin),每个容器都有一个给定的头部元素。

对于NonEmptyList,它的实现方式是copure返回列表的头部,cojoin返回一个由列表组成的列表,其中这个列表在头部,而所有剩余的元素在尾部。

例如(我将NonEmptyList缩写为Nel):

Nel(1,2,3).copure = 1
Nel(1,2,3).cojoin = Nel(Nel(1,2,3),Nel(2,3),Nel(3))
=>= 函数是 coKleisli 组合。如果你只知道 W 是一个 Comonad,如何组合两个函数 f: W[A] => Bg: W[B] => C?虽然 f 的输入类型和 g 的输出类型不兼容,但你可以使用 map(f) 得到 W[W[A]] => W[B],然后将其与 g 组合。现在,给定一个 W[A],你可以使用 cojoin 将其转换为 W[W[A]],以便传递给该函数。因此,唯一合理的组合是执行以下操作的函数 k
k(x) = g(x.cojoin.map(f))

因此,针对您的非空列表:

g(Nel(1,2,3).cojoin.map(f))
= g(Nel(Nel(1,2,3),Nel(2,3),Nel(3)).map(f))
= g(Nel("Nel(1,2,3)X","Nel(2,3)X","Nel(3)X"))
= BigInt(Nel("Nel(1,2,3)X","Nel(2,3)X","Nel(3)X").map(_.length).sum)
= BigInt(Nel(11,9,7).sum)
= 27

谢谢您的回答 - 我已经接受了 retronym 的回答,因为他回答了使用案例的请求,但这个回答仍然很好! - oxbow_lakes
没问题,我已经编辑了retronym的答案,加入了一个cokleisli组合的例子。 - Apocalisp
我相信你的意思是“你可以使用map(f)来得到W[W[A]] => W[B]”,但很遗憾我没有编辑权限。 - Elazar Leibovich

9

Cojoin也适用于scalaz.Treescalaz.TreeLoc。这可以被利用来找到从树的根节点到每个叶子节点的所有路径流。

def leafPaths[T](tree: Tree[T]): Stream[Stream[T]]
  = tree.loc.cojoin.toTree.flatten.filter(_.isLeaf).map(_.path)

使用coKleisli箭头组合,例如我们可以这样做:
def leafDist[A] = (cokleisli(leafPaths[A]) &&& cokleisli(_.rootLabel))
  =>= (_.map(s => (s._2, s._1.map(_.length).max)))

leafDist函数接受一棵树,返回一个新的树,其中每个节点都被标注为其到叶子节点的最大距离。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接