WIP, now with streaming DFS
All checks were successful
/ Build and Release Library (push) Successful in 1m22s
All checks were successful
/ Build and Release Library (push) Successful in 1m22s
This commit is contained in:
parent
f185bed5f5
commit
86c067f3fa
13 changed files with 338 additions and 73 deletions
14
build.sbt
14
build.sbt
|
|
@ -30,6 +30,10 @@ val Deps = new {
|
|||
val Core: ModuleID = "org.typelevel" %% "cats-core" % "2.13.0"
|
||||
}
|
||||
|
||||
val Fs2 = new {
|
||||
val Core: ModuleID = "co.fs2" %% "fs2-core" % "3.12.2"
|
||||
}
|
||||
|
||||
val Gs = new {
|
||||
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.3.3"
|
||||
}
|
||||
|
|
@ -46,7 +50,7 @@ lazy val testSettings = Seq(
|
|||
|
||||
lazy val `gs-graph` = project
|
||||
.in(file("."))
|
||||
.aggregate(core, cats)
|
||||
.aggregate(core, cats, fs2)
|
||||
.settings(sharedSettings)
|
||||
.settings(testSettings)
|
||||
.settings(name := s"${gsProjectName.value}-v${semVerMajor.value}")
|
||||
|
|
@ -64,3 +68,11 @@ lazy val cats = project
|
|||
.settings(testSettings)
|
||||
.settings(name := s"${gsProjectName.value}-cats-v${semVerMajor.value}")
|
||||
.settings(libraryDependencies ++= Seq(Deps.Cats.Core))
|
||||
|
||||
lazy val fs2 = project
|
||||
.in(file("modules/fs2"))
|
||||
.dependsOn(core)
|
||||
.settings(sharedSettings)
|
||||
.settings(testSettings)
|
||||
.settings(name := s"${gsProjectName.value}-fs2-v${semVerMajor.value}")
|
||||
.settings(libraryDependencies ++= Seq(Deps.Fs2.Core))
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package graph.gs.v0.cats.syntax
|
||||
package gs.graph.v0.cats.syntax
|
||||
|
||||
import cats.Monad
|
||||
import gs.graph.v0.Graph
|
||||
import gs.graph.v0.Vertex
|
||||
import gs.graph.v0.cats.GraphTraversalCats
|
||||
import gs.graph.v0.data.DataDigraph
|
||||
|
||||
extension (graph: Graph)
|
||||
|
||||
|
|
@ -13,12 +12,3 @@ extension (graph: Graph)
|
|||
|
||||
def dfs[F[_]: Monad, Output](visit: Vertex => F[Output]): F[List[Output]] =
|
||||
GraphTraversalCats.dfs[F, Output](graph, visit)
|
||||
|
||||
extension [A](graph: DataDigraph[A])
|
||||
|
||||
def dfsUnit[F[_]: Monad](visit: (Vertex, A) => F[Unit]): F[Unit] =
|
||||
GraphTraversalCats.dfsUnit[F, A](graph, visit)
|
||||
|
||||
def dfs[F[_]: Monad, Output](visit: (Vertex, A) => F[Output])
|
||||
: F[List[Output]] =
|
||||
GraphTraversalCats.dfs[F, A, Output](graph, visit)
|
||||
|
|
|
|||
|
|
@ -47,4 +47,11 @@ object Edge:
|
|||
|
||||
given CanEqual[Edge, Edge] = CanEqual.derived
|
||||
|
||||
def apply(
|
||||
v1: Vertex,
|
||||
v2: Vertex
|
||||
): Edge = new Edge(v1, v2)
|
||||
|
||||
def apply(vs: (Vertex, Vertex)): Edge = new Edge(vs._1, vs._2)
|
||||
|
||||
end Edge
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ package gs.graph.v0
|
|||
*
|
||||
* See:
|
||||
* - [[gs.graph.v0.directed.DirectedGraph]]
|
||||
* - [[gs.graph.v0.UndirectedGraph]]
|
||||
*
|
||||
* @param numberOfVertices
|
||||
* The number of [[Vertex]] present in this Graph.
|
||||
|
|
@ -31,6 +32,11 @@ trait Graph:
|
|||
*/
|
||||
def isEmpty: Boolean = numberOfVertices == Size.Zero
|
||||
|
||||
/** @return
|
||||
* The roots that should be used for traversal operations.
|
||||
*/
|
||||
def selectRoots(): Vector[Vertex]
|
||||
|
||||
/** Get the neighbors for any given vertex.
|
||||
*
|
||||
* @param vertex
|
||||
|
|
@ -52,19 +58,3 @@ trait Graph:
|
|||
case _ => false
|
||||
|
||||
end Graph
|
||||
|
||||
object Graph:
|
||||
|
||||
/** The empty graph. Contains no vertices and no edges.
|
||||
*/
|
||||
object Empty extends Graph:
|
||||
|
||||
override def numberOfVertices: Size = Size.Zero
|
||||
|
||||
override def adjacency: Adjacency = Adjacency.Empty
|
||||
|
||||
override def disposition: GraphDisposition = GraphDisposition.Undirected
|
||||
|
||||
end Empty
|
||||
|
||||
end Graph
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package gs.graph.v0
|
||||
|
||||
import gs.graph.v0.data.DataDigraph
|
||||
import gs.graph.v0.data.AnyGraphWithData
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.collection.mutable.Stack
|
||||
|
||||
|
|
@ -74,11 +74,35 @@ object GraphTraversal:
|
|||
|
||||
output.toList
|
||||
|
||||
def dfsFold[Acc](
|
||||
graph: Graph,
|
||||
initial: Acc,
|
||||
f: (Acc, Vertex) => Acc
|
||||
): Acc =
|
||||
var acc = initial
|
||||
val s = Stack.empty[Vertex]
|
||||
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
||||
|
||||
graph.selectRoots().foreach { root =>
|
||||
s.push(root)
|
||||
|
||||
while !s.isEmpty
|
||||
do
|
||||
val v = s.pop()
|
||||
if !discovered(v.ordinal) then
|
||||
acc = f(acc, v)
|
||||
discovered(v.ordinal) = true
|
||||
graph.neighbors(v).foreach(w => s.push(w))
|
||||
else ()
|
||||
}
|
||||
|
||||
acc
|
||||
|
||||
/** Depth-first search that executes a side-effecting function on each
|
||||
* [[Vertex]], accepting the data stored for that [[Vertex]] as input.
|
||||
*
|
||||
* This implementation performs DFS for _each root_ in the input
|
||||
* [[DataDigraph]].
|
||||
* [[AnyGraphWithData]].
|
||||
*
|
||||
* @param graph
|
||||
* The input [[Graph]] on which to run DFS.
|
||||
|
|
@ -86,22 +110,22 @@ object GraphTraversal:
|
|||
* The visitor function.
|
||||
*/
|
||||
def dfs[A](
|
||||
digraph: DataDigraph[A],
|
||||
graph: AnyGraphWithData[A],
|
||||
visit: (Vertex, A) => Unit
|
||||
): Unit =
|
||||
val s = Stack.empty[Vertex]
|
||||
val discovered = Array.fill(digraph.numberOfVertices.value)(false)
|
||||
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
||||
|
||||
digraph.roots.foreach { root =>
|
||||
graph.selectRoots().foreach { root =>
|
||||
s.push(root)
|
||||
|
||||
while !s.isEmpty
|
||||
do
|
||||
val v = s.pop()
|
||||
if !discovered(v.ordinal) then
|
||||
val _ = visit(v, digraph.data(v.ordinal))
|
||||
val _ = visit(v, graph.data(v.ordinal))
|
||||
discovered(v.ordinal) = true
|
||||
digraph.neighbors(v).foreach(w => s.push(w))
|
||||
graph.neighbors(v).foreach(w => s.push(w))
|
||||
else ()
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +135,7 @@ object GraphTraversal:
|
|||
* some output, accepting the data stored for that [[Vertex]] as input.
|
||||
*
|
||||
* This implementation performs DFS for _each root_ in the input
|
||||
* [[DataDigraph]].
|
||||
* [[AnyGraphWithData]].
|
||||
*
|
||||
* @param graph
|
||||
* The input [[Graph]] on which to run DFS.
|
||||
|
|
@ -121,26 +145,50 @@ object GraphTraversal:
|
|||
* List of output, assembled in the order that vertices were visited.
|
||||
*/
|
||||
def dfs[A, Out](
|
||||
digraph: DataDigraph[A],
|
||||
graph: AnyGraphWithData[A],
|
||||
visit: (Vertex, A) => Out
|
||||
): List[Out] =
|
||||
val output = ListBuffer.empty[Out]
|
||||
val s = Stack.empty[Vertex]
|
||||
val discovered = Array.fill(digraph.numberOfVertices.value)(false)
|
||||
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
||||
|
||||
digraph.roots.foreach { root =>
|
||||
graph.selectRoots().foreach { root =>
|
||||
s.push(root)
|
||||
|
||||
while !s.isEmpty
|
||||
do
|
||||
val v = s.pop()
|
||||
if !discovered(v.ordinal) then
|
||||
val _ = output.addOne(visit(v, digraph.data(v.ordinal)))
|
||||
val _ = output.addOne(visit(v, graph.data(v.ordinal)))
|
||||
discovered(v.ordinal) = true
|
||||
digraph.neighbors(v).foreach(w => s.push(w))
|
||||
graph.neighbors(v).foreach(w => s.push(w))
|
||||
else ()
|
||||
}
|
||||
|
||||
output.toList
|
||||
|
||||
def dfsFold[A, Acc](
|
||||
graph: AnyGraphWithData[A],
|
||||
initial: Acc,
|
||||
f: (Acc, A) => Acc
|
||||
): Acc =
|
||||
var acc = initial
|
||||
val s = Stack.empty[Vertex]
|
||||
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
||||
|
||||
graph.selectRoots().foreach { root =>
|
||||
s.push(root)
|
||||
|
||||
while !s.isEmpty
|
||||
do
|
||||
val v = s.pop()
|
||||
if !discovered(v.ordinal) then
|
||||
acc = f(acc, graph.data(v.ordinal))
|
||||
discovered(v.ordinal) = true
|
||||
graph.neighbors(v).foreach(w => s.push(w))
|
||||
else ()
|
||||
}
|
||||
|
||||
acc
|
||||
|
||||
end GraphTraversal
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@ object Size:
|
|||
*/
|
||||
final val Zero: Size = new Size(0)
|
||||
|
||||
/** The constant size: 1.
|
||||
*/
|
||||
final val One: Size = new Size(1)
|
||||
|
||||
/** Instantiate a new [[Size]].
|
||||
*
|
||||
* Throws an exception if a negative input is given.
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ class UndirectedGraph(
|
|||
/** @inheritDocs
|
||||
*/
|
||||
final override val disposition: GraphDisposition = GraphDisposition.Undirected
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def selectRoots(): Vector[Vertex] =
|
||||
if numberOfVertices == Size.Zero then Vector.empty else Vector(Vertex.Zero)
|
||||
|
||||
end UndirectedGraph
|
||||
|
||||
object UndirectedGraph:
|
||||
|
|
|
|||
|
|
@ -1,35 +1,18 @@
|
|||
package gs.graph.v0.data
|
||||
|
||||
import gs.graph.v0.Adjacency
|
||||
import gs.graph.v0.GraphDisposition
|
||||
import gs.graph.v0.Graph
|
||||
import gs.graph.v0.GraphException
|
||||
import gs.graph.v0.Size
|
||||
import gs.graph.v0.Vertex
|
||||
|
||||
/** Trait that describes _any_ graph (undirected, directed, or other
|
||||
* specializations of those such as DAGs) that is correlated with data.
|
||||
*/
|
||||
trait AnyGraphWithData[A]:
|
||||
trait AnyGraphWithData[A] extends Graph:
|
||||
/** @return
|
||||
* The data stored by each [[gs.graph.v0.Vertex]] in this graph.
|
||||
*/
|
||||
def data: Vector[A]
|
||||
|
||||
/** @return
|
||||
* The number of vertices present in this graph.
|
||||
*/
|
||||
def numberOfVertices: Size
|
||||
|
||||
/** @return
|
||||
* The [[Adjacency]] that describes this graph.
|
||||
*/
|
||||
def adjacency: Adjacency
|
||||
|
||||
/** @return
|
||||
* The underlying disposition of this graph.
|
||||
*/
|
||||
def disposition: GraphDisposition
|
||||
|
||||
/** Retrieve the data associated with some [[Vertex]].
|
||||
*
|
||||
* This implementation throws an exception if the input [[Vertex]] is out of
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ class Digraph(
|
|||
*/
|
||||
override val disposition: GraphDisposition = GraphDisposition.Directed
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def selectRoots(): Vector[Vertex] = roots
|
||||
|
||||
override def equals(that: Any): Boolean =
|
||||
that match
|
||||
case other: Digraph =>
|
||||
|
|
@ -39,8 +43,17 @@ object Digraph:
|
|||
|
||||
given CanEqual[Digraph, Digraph] = CanEqual.derived
|
||||
|
||||
/** @return
|
||||
* An empty [[Digraph]].
|
||||
*/
|
||||
def empty(): Digraph = new Digraph(Size.Zero, Adjacency.Empty, Vector.empty)
|
||||
|
||||
/** @return
|
||||
* A [[Digraph]] with a single [[Vertex]] and no edges.
|
||||
*/
|
||||
def single(): Digraph =
|
||||
new Digraph(Size.One, Adjacency(Vector(Vector.empty)), Vector(Vertex.Zero))
|
||||
|
||||
def fromEdges(
|
||||
numberOfVertices: Size,
|
||||
edges: Iterable[Edge]
|
||||
|
|
|
|||
|
|
@ -6,18 +6,18 @@ import gs.graph.v0.GraphException
|
|||
import gs.graph.v0.Size
|
||||
import gs.graph.v0.Vertex
|
||||
|
||||
class SingleRootDirectedGraph(
|
||||
class SingleRootDigraph(
|
||||
n: Size,
|
||||
a: Adjacency,
|
||||
r: Vertex
|
||||
) extends Digraph(n, a, Vector(r))
|
||||
|
||||
object SingleRootDirectedGraph:
|
||||
object SingleRootDigraph:
|
||||
|
||||
def fromDirectedGraph(dg: Digraph): Option[SingleRootDirectedGraph] =
|
||||
def fromDirectedGraph(dg: Digraph): Option[SingleRootDigraph] =
|
||||
if dg.roots.size == 1 then
|
||||
Some(
|
||||
new SingleRootDirectedGraph(
|
||||
new SingleRootDigraph(
|
||||
dg.numberOfVertices,
|
||||
dg.adjacency,
|
||||
dg.roots(0)
|
||||
|
|
@ -29,9 +29,9 @@ object SingleRootDirectedGraph:
|
|||
numberOfVertices: Size,
|
||||
edges: Iterable[Edge],
|
||||
root: Vertex
|
||||
): SingleRootDirectedGraph =
|
||||
): SingleRootDigraph =
|
||||
if root < numberOfVertices then
|
||||
new SingleRootDirectedGraph(
|
||||
new SingleRootDigraph(
|
||||
numberOfVertices,
|
||||
Adjacency.fromDirectedEdges(numberOfVertices, edges),
|
||||
root
|
||||
|
|
@ -41,11 +41,11 @@ object SingleRootDirectedGraph:
|
|||
def fromEdges(
|
||||
numberOfVertices: Size,
|
||||
edges: Iterable[Edge]
|
||||
): Option[SingleRootDirectedGraph] =
|
||||
): Option[SingleRootDigraph] =
|
||||
val roots = Digraph.findRootsForDirectedEdges(numberOfVertices, edges)
|
||||
if roots.size == 1 then
|
||||
Some(
|
||||
new SingleRootDirectedGraph(
|
||||
new SingleRootDigraph(
|
||||
numberOfVertices,
|
||||
Adjacency.fromDirectedEdges(numberOfVertices, edges),
|
||||
roots(0)
|
||||
|
|
@ -56,9 +56,9 @@ object SingleRootDirectedGraph:
|
|||
def fromAdjacencyUnsafe(
|
||||
adjacency: Adjacency,
|
||||
root: Vertex
|
||||
): SingleRootDirectedGraph =
|
||||
): SingleRootDigraph =
|
||||
if root < adjacency.numberOfVertices then
|
||||
new SingleRootDirectedGraph(
|
||||
new SingleRootDigraph(
|
||||
adjacency.numberOfVertices,
|
||||
adjacency,
|
||||
root
|
||||
|
|
@ -67,11 +67,11 @@ object SingleRootDirectedGraph:
|
|||
|
||||
def fromAdjacency(
|
||||
adjacency: Adjacency
|
||||
): Option[SingleRootDirectedGraph] =
|
||||
): Option[SingleRootDigraph] =
|
||||
val roots = adjacency.findRoots()
|
||||
if roots.size == 1 then
|
||||
Some(
|
||||
new SingleRootDirectedGraph(
|
||||
new SingleRootDigraph(
|
||||
adjacency.numberOfVertices,
|
||||
adjacency,
|
||||
roots(0)
|
||||
|
|
@ -79,4 +79,4 @@ object SingleRootDirectedGraph:
|
|||
)
|
||||
else None
|
||||
|
||||
end SingleRootDirectedGraph
|
||||
end SingleRootDigraph
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package gs.graph.v0.syntax
|
||||
|
||||
import gs.graph.v0.Graph
|
||||
import gs.graph.v0.GraphTraversal
|
||||
import gs.graph.v0.Vertex
|
||||
import gs.graph.v0.data.AnyGraphWithData
|
||||
|
||||
extension (graph: Graph)
|
||||
|
||||
/** Visit vertex node in this graph, using Depth-First Search (DFS) as the
|
||||
* traversal method. For each vertex, execute the given visitor function
|
||||
* against the vertex.
|
||||
*
|
||||
* In this variant, the visitor is a side-effecting function that does not
|
||||
* return any value.
|
||||
*
|
||||
* @param visit
|
||||
* Side-effecting function to execute for each graph vertex.
|
||||
*/
|
||||
def visitDfs(visit: Vertex => Unit): Unit =
|
||||
GraphTraversal.dfs(graph, visit)
|
||||
|
||||
/** Visit vertex node in this graph, using Depth-First Search (DFS) as the
|
||||
* traversal method. For each vertex, execute the given visitor function
|
||||
* against the vertex.
|
||||
*
|
||||
* In this variant, the visitor produces some output, and the list of all
|
||||
* such outputs is collected and returned.
|
||||
*
|
||||
* @param visit
|
||||
* Function to execute for each graph vertex.
|
||||
* @return
|
||||
* The list of produced values.
|
||||
*/
|
||||
def visitDfs[Out](visit: Vertex => Out): List[Out] =
|
||||
GraphTraversal.dfs[Out](graph, visit)
|
||||
|
||||
/** Perform a fold on this graph, using Depth-First Search (DFS) as the
|
||||
* traversal method.
|
||||
*
|
||||
* @param initial
|
||||
* The initial value for the accumulator.
|
||||
* @param f
|
||||
* The function to execute on each vertex in the graph.
|
||||
* @return
|
||||
* The final calculated value.
|
||||
*/
|
||||
def foldDfs[Acc](initial: Acc)(f: (Acc, Vertex) => Acc): Acc =
|
||||
GraphTraversal.dfsFold[Acc](graph, initial, f)
|
||||
|
||||
extension [A](graph: AnyGraphWithData[A])
|
||||
|
||||
/** Visit vertex node in this graph, using Depth-First Search (DFS) as the
|
||||
* traversal method. For each vertex, execute the given visitor function
|
||||
* against the vertex and the data it stores.
|
||||
*
|
||||
* In this variant, the visitor is a side-effecting function that does not
|
||||
* return any value.
|
||||
*
|
||||
* @param visit
|
||||
* Side-effecting function to execute for each graph vertex.
|
||||
*/
|
||||
def visitDfs(visit: (Vertex, A) => Unit): Unit =
|
||||
GraphTraversal.dfs[A](graph, visit)
|
||||
|
||||
/** Visit vertex node in this graph, using Depth-First Search (DFS) as the
|
||||
* traversal method. For each vertex, execute the given visitor function
|
||||
* against the vertex and the data it stores.
|
||||
*
|
||||
* In this variant, the visitor produces some output, and the list of all
|
||||
* such outputs is collected and returned.
|
||||
*
|
||||
* @param visit
|
||||
* Function to execute for each graph vertex.
|
||||
* @return
|
||||
* The list of produced values.
|
||||
*/
|
||||
def visitDfs[Out](visit: (Vertex, A) => Out): List[Out] =
|
||||
GraphTraversal.dfs[A, Out](graph, visit)
|
||||
|
||||
/** Perform a fold on this graph, using Depth-First Search (DFS) as the
|
||||
* traversal method.
|
||||
*
|
||||
* @param initial
|
||||
* The initial value for the accumulator.
|
||||
* @param f
|
||||
* The function to execute on each data point in the graph.
|
||||
* @return
|
||||
* The final calculated value.
|
||||
*/
|
||||
def foldDfs[Acc](initial: Acc)(f: (Acc, A) => Acc): Acc =
|
||||
GraphTraversal.dfsFold[A, Acc](graph, initial, f)
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
package gs.graph.v0.directed
|
||||
|
||||
import gs.graph.v0.Adjacency
|
||||
import gs.graph.v0.Edge
|
||||
import gs.graph.v0.Size
|
||||
import gs.graph.v0.Vertex
|
||||
import munit.*
|
||||
|
||||
class DagTests extends FunSuite:
|
||||
|
|
@ -9,3 +13,58 @@ class DagTests extends FunSuite:
|
|||
case Right(dag) => assertEquals(dag, Digraph.empty())
|
||||
case _ => fail("Expected the empty graph to be validated as a DAG.")
|
||||
}
|
||||
|
||||
test("should validate a graph with one node") {
|
||||
Dag.validate(Digraph.single()) match
|
||||
case Right(dag) => assertEquals(dag, Digraph.single())
|
||||
case _ =>
|
||||
fail("Expected a graph with one vertex to be validated as a DAG.")
|
||||
}
|
||||
|
||||
test("should validate a single-root graph") {
|
||||
val size = Size(8)
|
||||
val vs = (0 until size.value).map(Vertex(_))
|
||||
val digraph: Digraph = Digraph.fromAdjacency(
|
||||
Adjacency.fromDirectedEdges(
|
||||
numberOfVertices = size,
|
||||
edges = Seq(
|
||||
Edge(vs(0) -> vs(1)),
|
||||
Edge(vs(0) -> vs(2)),
|
||||
Edge(vs(0) -> vs(3)),
|
||||
Edge(vs(1) -> vs(4)),
|
||||
Edge(vs(2) -> vs(4)),
|
||||
Edge(vs(3) -> vs(5)),
|
||||
Edge(vs(4) -> vs(6)),
|
||||
Edge(vs(5) -> vs(6)),
|
||||
Edge(vs(6) -> vs(7))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Dag.validate(digraph) match
|
||||
case Right(dag) => assertEquals(dag, digraph)
|
||||
case _ => fail("Expected a single-root graph to be validated as a DAG.")
|
||||
}
|
||||
|
||||
test("should NOT validate a single-root digraph with a cycle") {
|
||||
val size = Size(4)
|
||||
val vs = (0 until size.value).map(Vertex(_))
|
||||
val digraph: Digraph = Digraph.fromAdjacency(
|
||||
Adjacency.fromDirectedEdges(
|
||||
numberOfVertices = size,
|
||||
edges = Seq(
|
||||
Edge(vs(0) -> vs(1)),
|
||||
Edge(vs(1) -> vs(2)),
|
||||
Edge(vs(2) -> vs(3)),
|
||||
Edge(vs(3) -> vs(1))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Dag.validate(digraph) match
|
||||
case Right(_) =>
|
||||
fail(
|
||||
"Expected a single-root digraph with a cycle to fail DAG validation."
|
||||
)
|
||||
case _ => ()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
package gs.graph.v0.fs2
|
||||
|
||||
import cats.effect.Sync
|
||||
import fs2.Pull
|
||||
import fs2.Stream
|
||||
import gs.graph.v0.Graph
|
||||
import gs.graph.v0.Size
|
||||
import gs.graph.v0.Vertex
|
||||
import scala.collection.mutable.Stack
|
||||
|
||||
object GraphTraversalFs2:
|
||||
|
||||
def dfs[F[_]: Sync, Out](
|
||||
graph: Graph,
|
||||
visit: Vertex => F[Out]
|
||||
): Stream[F, Out] =
|
||||
val state = new DfsState(graph.numberOfVertices)
|
||||
graph
|
||||
.selectRoots()
|
||||
.map(root => pull(graph, state, visit, root).stream.unNoneTerminate)
|
||||
.reduce(_ ++ _)
|
||||
|
||||
private def pull[F[_]: Sync, Out](
|
||||
graph: Graph,
|
||||
state: DfsState,
|
||||
visit: Vertex => F[Out],
|
||||
current: Vertex
|
||||
): Pull[F, Option[Out], Unit] =
|
||||
Pull.eval(Sync[F].delay(state.isDiscovered(current))).flatMap { discovered =>
|
||||
if discovered then Pull.output1(None) >> Pull.done
|
||||
else
|
||||
Pull.eval(Sync[F].delay(state.discover(current)))
|
||||
>> Pull.eval(visit(current)).flatMap(out => Pull.output1(Some(out)))
|
||||
>> graph
|
||||
.neighbors(current)
|
||||
.foldLeft(Pull.done: Pull[F, Option[Out], Unit]) {
|
||||
(
|
||||
acc,
|
||||
neighbor
|
||||
) => acc >> pull(graph, state, visit, neighbor)
|
||||
}
|
||||
}
|
||||
|
||||
final private class DfsState(n: Size):
|
||||
|
||||
val stack: Stack[Vertex] = Stack.empty
|
||||
val discovered: Array[Boolean] = Array.fill(n.value)(false)
|
||||
|
||||
def push(vertex: Vertex): Unit = stack.push(vertex)
|
||||
|
||||
def pop(): Vertex = stack.pop()
|
||||
|
||||
def isDiscovered(vertex: Vertex): Boolean =
|
||||
discovered(vertex.ordinal)
|
||||
|
||||
def discover(vertex: Vertex): Unit =
|
||||
discovered(vertex.ordinal) = true
|
||||
|
||||
end DfsState
|
||||
|
||||
end GraphTraversalFs2
|
||||
Loading…
Add table
Reference in a new issue