From 14e33a24f924023988ce1d2d321a970f4a11f12a Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Sun, 28 Dec 2025 21:39:16 -0600 Subject: [PATCH] WIP: Adding streaming traversal. --- .../main/scala/gs/graph/v0/Adjacency.scala | 5 +++ .../scala/gs/graph/v0/UndirectedGraph.scala | 9 +++- .../scala/gs/graph/v0/directed/Digraph.scala | 9 ++-- .../scala/gs/graph/v0/directed/DagTests.scala | 8 ++-- .../gs/graph/v0/fs2/GraphTraversalFs2.scala | 12 +++--- .../scala/gs/graph/v0/fs2/Fs2DfsTests.scala | 43 +++++++++++++++++++ 6 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 modules/fs2/src/test/scala/gs/graph/v0/fs2/Fs2DfsTests.scala diff --git a/modules/core/src/main/scala/gs/graph/v0/Adjacency.scala b/modules/core/src/main/scala/gs/graph/v0/Adjacency.scala index 98a4151..7bb4fd6 100644 --- a/modules/core/src/main/scala/gs/graph/v0/Adjacency.scala +++ b/modules/core/src/main/scala/gs/graph/v0/Adjacency.scala @@ -81,6 +81,11 @@ object Adjacency: */ final val Empty: Adjacency = new Adjacency(Vector.empty) + /** @return + * The single-vertex [[Adjacency]] list. + */ + final val Single: Adjacency = new Adjacency(Vector(Vector.empty)) + /** Calculate an [[Adjacency]] from some collection of [[Edge]]. * * @param numberOfVertices diff --git a/modules/core/src/main/scala/gs/graph/v0/UndirectedGraph.scala b/modules/core/src/main/scala/gs/graph/v0/UndirectedGraph.scala index cc96214..4795f5d 100644 --- a/modules/core/src/main/scala/gs/graph/v0/UndirectedGraph.scala +++ b/modules/core/src/main/scala/gs/graph/v0/UndirectedGraph.scala @@ -27,6 +27,13 @@ object UndirectedGraph: /** @return * An empty [[UndirectedGraph]]. */ - def empty(): UndirectedGraph = new UndirectedGraph(Size.Zero, Adjacency.Empty) + final val Empty: UndirectedGraph = + new UndirectedGraph(Size.Zero, Adjacency.Empty) + + /** @return + * An [[UndirectedGraph]] with a single vertex. + */ + final val Single: UndirectedGraph = + new UndirectedGraph(Size.One, Adjacency.Single) end UndirectedGraph diff --git a/modules/core/src/main/scala/gs/graph/v0/directed/Digraph.scala b/modules/core/src/main/scala/gs/graph/v0/directed/Digraph.scala index 980b20b..7da4dc5 100644 --- a/modules/core/src/main/scala/gs/graph/v0/directed/Digraph.scala +++ b/modules/core/src/main/scala/gs/graph/v0/directed/Digraph.scala @@ -46,13 +46,14 @@ object Digraph: /** @return * An empty [[Digraph]]. */ - def empty(): Digraph = new Digraph(Size.Zero, Adjacency.Empty, Vector.empty) + final val Empty: Digraph = + new Digraph(Size.Zero, Adjacency.Empty, Vector.empty) /** @return - * A [[Digraph]] with a single [[Vertex]] and no edges. + * A [[Digraph]] with one vertex. */ - def single(): Digraph = - new Digraph(Size.One, Adjacency(Vector(Vector.empty)), Vector(Vertex.Zero)) + final val Single: Digraph = + new Digraph(Size.One, Adjacency.Single, Vector(Vertex.Zero)) def fromEdges( numberOfVertices: Size, diff --git a/modules/core/src/test/scala/gs/graph/v0/directed/DagTests.scala b/modules/core/src/test/scala/gs/graph/v0/directed/DagTests.scala index a1adc93..40cd965 100644 --- a/modules/core/src/test/scala/gs/graph/v0/directed/DagTests.scala +++ b/modules/core/src/test/scala/gs/graph/v0/directed/DagTests.scala @@ -9,14 +9,14 @@ import munit.* class DagTests extends FunSuite: test("should validate an empty graph") { - Dag.validate(Digraph.empty()) match - case Right(dag) => assertEquals(dag, Digraph.empty()) + Dag.validate(Digraph.Empty) match + 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()) + 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.") } diff --git a/modules/fs2/src/main/scala/gs/graph/v0/fs2/GraphTraversalFs2.scala b/modules/fs2/src/main/scala/gs/graph/v0/fs2/GraphTraversalFs2.scala index e0473bd..a01395e 100644 --- a/modules/fs2/src/main/scala/gs/graph/v0/fs2/GraphTraversalFs2.scala +++ b/modules/fs2/src/main/scala/gs/graph/v0/fs2/GraphTraversalFs2.scala @@ -14,11 +14,13 @@ object GraphTraversalFs2: 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(_ ++ _) + if graph.isEmpty then Stream.empty + else + 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, diff --git a/modules/fs2/src/test/scala/gs/graph/v0/fs2/Fs2DfsTests.scala b/modules/fs2/src/test/scala/gs/graph/v0/fs2/Fs2DfsTests.scala new file mode 100644 index 0000000..c6328be --- /dev/null +++ b/modules/fs2/src/test/scala/gs/graph/v0/fs2/Fs2DfsTests.scala @@ -0,0 +1,43 @@ +package gs.graph.v0.fs2 + +import cats.effect.IO +import cats.effect.unsafe.IORuntime +import gs.graph.v0.UndirectedGraph +import gs.graph.v0.Vertex +import gs.graph.v0.directed.Digraph +import munit.* + +class Fs2DfsTests extends FunSuite: + given IORuntime = IORuntime.global + + private def iotest( + name: String + )( + body: => IO[Unit] + )( + using + Location + ): Unit = + test(name)(body.unsafeRunSync()) + + iotest("(DFS) should return an empty stream for an empty graph") { + val s = GraphTraversalFs2.dfs( + UndirectedGraph.Empty, + _ => IO.raiseError(IllegalStateException("Should not reach this point.")) + ) + s.compile.last.map(result => assertEquals(result, None)) + } + + iotest( + "(DFS) should return a stream of one for a graph with a single vertex" + ) { + val s1 = GraphTraversalFs2.dfs(UndirectedGraph.Single, v => IO(v)) + val s2 = GraphTraversalFs2.dfs(Digraph.Single, v => IO(v)) + + for + r1 <- s1.compile.toList + r2 <- s2.compile.toList + yield + assertEquals(r1, List(Vertex.Zero)) + assertEquals(r2, List(Vertex.Zero)) + }