使用State Monad在Scala中进行功能广度优先搜索

时间:2017-08-16 19:56:45

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

我正在尝试在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. 如何使此代码更高效/简洁/可读?

1 个答案:

答案 0 :(得分:1)

首先,我建议将与private def相关的所有bfs移动到bfs本身。这是仅用于实现另一个方法的方法的惯例。

其次,我建议不要在此问题上使用StateState(和大多数monad一样)是关于构图的。当你有很多东西需要访问同一个全局状态时,它很有用。在这种情况下,BfsState专门用于bfs,可能永远不会在其他地方使用(将类移到bfs也可能是个好主意),{{1 }}本身总是State,所以外部世界永远不会看到它。 (在很多情况下,这很好,但是这里的范围太小,run无用。)将State的逻辑拉入searchNode会更清晰本身。

第三,我不明白为什么你需要bfsCompnodesList,当你可以在discovered上致电_.toList完成你的计算。我已经在重新实现中留下了它,但是,如果此代码中还有更多内容尚未显示。

discovered

如果你认为你有充分的理由在这里使用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 } ,这是我的想法。 您使用Statedef searchNode的要点是它是纯粹且不可变的,因此它应该是State,否则每次使用时都会重建相同的val

你写道:

State

首先,Scala的语法是为了让您不需要围绕匿名函数node <- State[BfsState, Node](s => { val (n, newQ) = s.q.dequeue (n, s.copy(q = newQ)) }) ()而设计的:

{}

其次,这对我来说并非相当。使用for-syntax的一个好处是匿名函数对你来说是隐藏的,并且有最小的缩进。我只是写出来

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

脚注:让oldState <- get (node, newQ) = oldState.q.dequeue newState = oldState.copy(q = newQ) 成为Node的内部类是否有意义?只是一个建议。