解释scalaz-7中的Traverse [List]实现

时间:2012-03-15 04:16:28

标签: scala typeclass traversal scalaz applicative

我正在尝试理解scalaz-seven中的traverseImpl实现:

def traverseImpl[F[_], A, B](l: List[A])(f: A => F[B])(implicit F: Applicative[F]) = {
  DList.fromList(l).foldr(F.point(List[B]())) {
     (a, fbs) => F.map2(f(a), fbs)(_ :: _)
  }
}

有人可以解释ListApplicative的互动方式吗?最后,我希望能够为Traverse实现其他实例。

3 个答案:

答案 0 :(得分:9)

应用程序允许您将上下文中的函数应用于上下文中的值。例如,您可以将some((i: Int) => i + 1)应用于some(3)并获取some(4)。我们暂时忘记这一点。我稍后会回来。

列表有两个表示形式,它们是Nilhead :: tail。您可能习惯使用foldLeft折叠它,但还有另一种方法可以折叠它:

def foldr[A, B](l: List[A], acc0: B, f: (A, B) => B): B = l match {
   case Nil => acc0
   case x :: xs => f(x, foldr(xs, acc0, f))
}

给定List(1, 2)我们将从右侧开始应用函数的列表折叠 - 即使我们真的从左侧解构列表!

f(1, f(2, Nil))

这可用于计算列表的长度。鉴于List(1, 2)

foldr(List(1, 2), 0, (i: Int, acc: Int) => 1 + acc)
// returns 2

这也可用于创建另一个列表:

foldr[Int, List[Int]](List(1, 2), List[Int](), _ :: _)
//List[Int] = List(1, 2)

因此,给定一个空列表和::函数,我们可以创建另一个列表。如果我们的元素位于某些上下文中会怎样?如果我们的上下文是一个应用程序,那么我们仍然可以在该上下文中应用我们的元素和::。继续使用List(1, 2)Option作为我们的申请。我们从some(List[Int]()))开始,我们要在::上下文中应用Option函数。这就是F.map2的作用。它在Option上下文中需要两个值,将两个参数的提供函数放入Option上下文并将它们一起应用。

所以在上下文之外我们有(2, Nil) => 2 :: Nil

在上下文中,我们有:(Some(2), Some(Nil)) => Some(2 :: Nil)

回到原来的问题:

// do a foldr 
DList.fromList(l).foldr(F.point(List[B]())) {
  // starting with an empty list in its applicative context F.point(List[B]())
  (a, fbs) => F.map2(f(a), fbs)(_ :: _)
  // Apply the `::` function to the two values in the context
}

我不确定为何使用差异DList。我所看到的是,它使用蹦床,所以希望这使得这个实现工作没有吹嘘堆栈,但我没有尝试过,所以我不知道。

关于如此实现正确折叠的有趣部分是,我认为它为您提供了一种使用catamorphisms为代数数据类型实现遍历的方法。

例如:

trait Tree[+A]
object Leaf extends Tree[Nothing]
case class Node[A](a: A, left: Tree[A], right: Tree[A]) extends Tree[A]

折叠将被定义为这样(实际上遵循与List相同的方法):

def fold[A, B](tree: Tree[A], valueForLeaf: B, functionForNode: (A, B, B) => B): B = {
  tree match {
    case Leaf => valueForLeaf
    case Node(a, left, right) => functionForNode(a, 
        fold(left, valueForLeaf, functionForNode), 
        fold(right, valueForLeaf, functionForNode)
      )
  }
}

并且遍历会将foldF.point(Leaf)一起使用并将其应用于Node.apply。虽然没有F.map3因此可能有点麻烦。

答案 1 :(得分:6)

这不容易掌握。我建议您阅读my blog post on the subject开头链接的文章。

我还在悉尼的最后一次功能编程会议上做了关于这个主题的演讲,你可以找到幻灯片here

如果我可以尝试用几句话来解释,traverse将逐个遍历列表中的每个元素,最终重新构建列表(_ :: _)但是累积/执行某种类型的F Applicative给出的“效果”。如果FState,则会跟踪某个州。如果F是与Monoid对应的应用,则会为列表中的每个元素聚合某种度量。

列表和应用程序的主要交互是map2应用程序,它接收F[B]元素,并根据{{1}的定义将其附加到其他F[List[B]]元素} F以及使用Applicative构造函数List作为要应用的特定函数。

从那里你可以看到,实现::的其他实例只是Traverse要遍历的数据结构的数据构造函数。如果你看一下链接的powerpoint演示文稿,你会看到一些带有二叉树遍历的幻灯片。

答案 2 :(得分:2)

List#foldRight为大型列表打击堆栈。在REPL中尝试这个:

List.range(0, 10000).foldRight(())((a, b) => ())

通常,您可以反转列表,使用foldLeft,然后反转结果以避免此问题。但是对于traverse,我们必须以正确的顺序处理元素,以确保正确处理效果。 DList是一种通过蹦床来实现这一目标的便捷方式。

最后,这些测试必须通过:

https://github.com/scalaz/scalaz/blob/scalaz-seven/tests/src/test/scala/scalaz/TraverseTest.scala#L13 https://github.com/scalaz/scalaz/blob/scalaz-seven/tests/src/test/scala/scalaz/std/ListTest.scala#L11 https://github.com/scalaz/scalaz/blob/scalaz-seven/core/src/main/scala/scalaz/Traverse.scala#L76