【问题标题】:Functional Breadth First Search in Scala with the State Monad使用 State Monad 在 Scala 中进行功能广度优先搜索
【发布时间】:2017-08-16 19:56:44
【问题描述】:

我正在尝试在 Scala 中实现功能性广度优先搜索,以计算给定节点与未加权图中所有其他节点之间的距离。我为此使用了 State Monad,其签名为:-

case class State[S,A](run:S => (A,S))

map、flatMap、sequence、modify 等其他函数与您在标准 State Monad 中找到的类似。

这是代码:-

case class Node(label: Int)

case class BfsState(q: Queue[Node], nodesList: List[Node], discovered: Set[Node], distanceFromSrc: Map[Node, Int]) {
  val isTerminated = q.isEmpty
}

case class Graph(adjList: Map[Node, List[Node]]) {
  def bfs(src: Node): (List[Node], Map[Node, Int]) = {
    val initialBfsState = BfsState(Queue(src), List(src), Set(src), Map(src -> 0))
    val output = bfsComp(initialBfsState)
    (output.nodesList,output.distanceFromSrc)
  }

  @tailrec
  private def bfsComp(currState:BfsState): BfsState = {
     if (currState.isTerminated) currState
     else bfsComp(searchNode.run(currState)._2)
  }

  private def searchNode: State[BfsState, Unit] = for {
    node <- State[BfsState, Node](s => {
      val (n, newQ) = s.q.dequeue
      (n, s.copy(q = newQ))
    })
    s <- get
    _ <- sequence(adjList(node).filter(!s.discovered(_)).map(n => {
      modify[BfsState](s => {
        s.copy(s.q.enqueue(n), n :: s.nodesList, s.discovered + n, s.distanceFromSrc + (n -> (s.distanceFromSrc(node) + 1)))
      })
    }))
  } yield ()
}   

请你给点建议:-

  1. searchNode 函数中的出列状态转换是否应该是 BfsState 本身的成员?
  2. 如何使这段代码更高效/简洁/可读?

【问题讨论】:

    标签: scala functional-programming breadth-first-search state-monad


    【解决方案1】:

    首先,我建议将所有与bfs 相关的private defs 移至bfs 本身。这是仅用于实现另一个方法的约定。

    其次,我建议不要使用State 来解决这个问题。 State(像大多数单子一样)是关于组合的。当您有许多东西都需要访问相同的全局状态时,它很有用。在这种情况下,BfsState 专用于bfs,可能永远不会在其他任何地方使用(最好也将类移至bfs),而State 本身始终是run ,所以外面的世界永远看不到它。 (在许多情况下,这很好,但这里的范围太小,State 无法使用。)将searchNode 的逻辑拉入bfsComp 本身会更清晰。

    第三,我不明白为什么你需要nodesListdiscovered,当你完成计算后可以在discovered 上调用_.toList。不过,我将它留在了我的重新实现中,以防您未显示此代码的更多内容。

    def bfsComp(old: BfsState): BfsState = {
      if(old.q.isEmpty) old // You don't need isTerminated, I think
      else {
        val (currNode, newQ) = old.q.dequeue
        val newState = old.copy(q = newQ)
        adjList(curNode)
          .filterNot(s.discovered) // Set[T] <: T => Boolean and filterNot means you don't need to write !s.discovered(_)
          .foldLeft(newState) { case (BfsState(q, nodes, discovered, distance), adjNode) =>
            BfsState(
              q.enqueue(adjNode),
              adjNode :: nodes,
              discovered + adjNode,
              distance + (adjNode -> (distance(currNode) + 1)
            )
          }
      }
    }
    
    def bfs(src: Node): (List[Node], Map[Node, Int]) = {
      // I suggest moving BfsState and bfsComp into this method
      val output = bfsComp(BfsState(Queue(src), List(src), Set(src), Map(src -> 0)))
      (output.nodesList, output.distanceFromSrc)
      // Could get rid of nodesList and say output.discovered.toList
    }
    

    如果您认为自己确实有充分的理由在此处使用State,以下是我的想法。 你使用def searchNodeState 的意义在于它是纯的和不可变的,所以它应该是 val,否则你每次使用都重构相同的 State

    你写:

    node <- State[BfsState, Node](s => {
      val (n, newQ) = s.q.dequeue
      (n, s.copy(q = newQ))
    })
    

    首先,Scala 的语法经过设计,因此您不需要在匿名函数周围同时使用 (){}

    node <- State[BfsState, Node] { s =>
      // ...
    }
    

    其次,这对我来说相当。使用 for 语法的一个好处是匿名函数对您隐藏并且缩进最少。我就写出来

    oldState <- get
    (node, newQ) = oldState.q.dequeue
    newState = oldState.copy(q = newQ)
    

    脚注:将Node 设为Graph 的内部类有意义吗?只是一个建议。

    【讨论】:

    • 感谢您详细而精彩的回答。我正在相应地重构代码。只有一个问题 1.) 您能否详细说明为什么 State Monad 在这里不是一个好主意,并举一些您认为这是个好主意的例子?
    • State 在这里不是很有用,因为它用得不多。在这种情况下,State 只有一个真正的值,而使用State 所增加的复杂性远远大于它提供的好处。如果您正在做更复杂的事情,State 会更有用,但在这里,它不是。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-01-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-11
    • 1970-01-01
    • 2010-10-25
    相关资源
    最近更新 更多