【问题标题】:Make recursive function stack - safe with Free Monads Trampoline in Scala?使用 Scala 中的 Free Monads Trampoline 制作递归函数堆栈 - 安全吗?
【发布时间】:2017-07-17 17:01:19
【问题描述】:

我已经为我的模型指定了特征:

sealed trait TreeStructureModel{
  val parentId: Option[Long]
  val title: String
  val id: Long
}

然后我从数据库中的记录构建一棵树:

trait SimpleTree[+TreeStructureModel]{
  val title: String
  val id: Long
}
trait Node[+TreeStructureModel] extends SimpleTree[TreeStructureModel]{
  val inner: List[SimpleTree[TreeStructureModel]]
}
trait Leaf[+TreeStructureModel] extends SimpleTree[TreeStructureModel]

case class NodeImp[T <: TreeStructureModel](title: String, inner: List[SimpleTree[T]], id: Long) extends Node[T]
case class LeafImp[T <: TreeStructureModel](title: String, id: Long) extends Leaf[T]

object SimpleTree{
  def apply[T <: TreeStructureModel](ls: List[T]): List[SimpleTree[T]] = {
    def build(ls: List[T], current: T): SimpleTree[T] = {
      val children = ls.filter{ v => v.parentId.isDefined && v.parentId.get == current.id}
      if(children.isEmpty){
        LeafImp(title = current.title, id = current.id)
      } else {
        val newLs = ls.filterNot{ v => v.parentId.isDefined && v.parentId.get == current.id}
        NodeImp(title = current.title, id = current.id, inner = children.map{ch => build(newLs, ch)})
    }
  }
    val roots = ls.filter{ v => v.parentId.isEmpty}
    val others = ls.filterNot{ v => v.parentId.isEmpty}
    roots.map(build(others, _))
  }
}

此代码工作正常,但使用非尾递归调用。所以,我担心它会在大量记录上失败。我发现了一个很棒的 article 在非尾递归上使用 Free monads Trampoline。 这看起来像是一种方法,但我无法重写我的代码以使其堆栈安全。在文章中的示例中,函数中只有一个递归调用,但在我的函数中可以有很多,用于构建一棵树。对 Free monads 更有经验的人可以帮我解决这个问题吗?这甚至可能吗?

【问题讨论】:

  • 列表的长度不是问题,但树的深度是。您可以将build 方法更改为返回Trampoline[SimpleTree],而不是children.map{ch =&gt; build(newLs, ch)} 使用children.traverse{ch =&gt; build(newLs, ch)},但您也可以通过自下而上(从叶子)构建树来直接递归实现它。跨度>
  • 你能提供更多细节吗?当将 build 方法的结果更改为 Trampoline[SimpleTree] 时,children.traverse{ ch => build(newLs, ch)} 在 build(newLs, ch) 上出现类型不匹配错误
  • 见我的回答。

标签: scala recursion scalaz scala-cats


【解决方案1】:

详细说明我的评论,使build 方法返回Trampoline 并使用traverse 代替map

import scalaz.Free.Trampoline
import scalaz.Trampoline
import scalaz.syntax.traverse._
import scalaz.std.list._

sealed trait TreeStructureModel{
  val parentId: Option[Long]
  val title: String
  val id: Long
}

trait SimpleTree[+TreeStructureModel]{
  val title: String
  val id: Long
}
trait Node[+TreeStructureModel] extends SimpleTree[TreeStructureModel]{
  val inner: List[SimpleTree[TreeStructureModel]]
}
trait Leaf[+TreeStructureModel] extends SimpleTree[TreeStructureModel]

case class NodeImp[T <: TreeStructureModel](title: String, inner: List[SimpleTree[T]], id: Long) extends Node[T]
case class LeafImp[T <: TreeStructureModel](title: String, id: Long) extends Leaf[T]

object SimpleTree{
  def apply[T <: TreeStructureModel](ls: List[T]): List[SimpleTree[T]] = {
    def build(ls: List[T], current: T): Trampoline[SimpleTree[T]] = {
      val children = ls.filter{ v => v.parentId.isDefined && v.parentId.get == current.id}
      if(children.isEmpty){
        Trampoline.done(LeafImp(title = current.title, id = current.id))
      } else {
        val newLs = ls.filterNot{ v => v.parentId.isDefined && v.parentId.get == current.id}
        children.traverse(build(newLs, _)).map(trees => NodeImp(title = current.title, id = current.id, inner = trees))
      }
    }
    val roots = ls.filter{ v => v.parentId.isEmpty}
    val others = ls.filterNot{ v => v.parentId.isEmpty}
    roots.map(build(others, _).run)
  }
}

请注意,为了使用Trampoline,我对您的代码做了一些必要的更改。我会进一步建议使用对partition 的单个调用,而不是一对filterfilterNot

直接使方法尾递归仍然是一个很好的练习。

【讨论】:

  • 非常感谢。使用分区的好答案和好建议
【解决方案2】:

您可以将函数重写为尾递归,而无需使用 scalaz。奥卡姆剃刀,你知道的……

 def build(
   ls: List[T], 
   kids: Map[Long, List[T]],
   result: Map[Long, SimpleTree[T]]
 ) = ls match {
   case Nil => result
   case head :: tail if result.contains(head.id) => build(tail, kids, result)
   case head :: tail =>          
     kids(head.id).partition(result.contains(_.id)) match {
       case (Nil, Nil) => 
         build(tail, kids, result + (head.id->LeafImp(head.title, head.id)))
       case (done, Nil) => 
         build(
           tail,
           kids, 
           result + 
           (head.id->NodeImp(head.title, head.id, done.map(_.id).map(result)))
         )
       case (_, missing) =>
         build(missing ++ tail, kids, result)
     }
  }

  def apply(ls: List[T]) = {
    val (roots, others) = list.partition(_.parentId.isEmpty)
    val nodes = build(ls, others.groupBy(_.parentId.get), Map.empty)
    roots.map(_.id).map(nodes)
  }

【讨论】:

  • 您的代码有问题。这不会返回一棵树,而是返回 Map of Leafs
  • build 返回所有节点(不仅仅是叶子)的 Map,apply 返回根列表。
  • 对我来说 build 总是返回 LeafImps 的 Map 并且 apply 只返回它们的根
  • 是的,还是谢谢你。我明白了,将使用您的版本来找出问题所在
  • 哦,我明白了。 tail.partition 中的条件是错误的......我修复了它。现在应该好多了。
猜你喜欢
  • 1970-01-01
  • 2019-09-09
  • 2018-10-03
  • 2018-09-24
  • 2017-06-18
  • 2017-03-12
  • 1970-01-01
  • 1970-01-01
  • 2023-03-27
相关资源
最近更新 更多