Refactoring to support cats. Adding documentation. Moving DAGs.
All checks were successful
/ Build and Release Library (push) Successful in 1m20s

This commit is contained in:
Pat Garrity 2025-12-10 22:17:11 -06:00
parent 7b1c110c75
commit 2dcb9f7c26
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
18 changed files with 345 additions and 12 deletions

View file

@ -13,6 +13,10 @@ ThisBuild / licenses := Seq(
"MIT" -> url("https://git.garrity.co/garrity-software/gs-graph/LICENSE") "MIT" -> url("https://git.garrity.co/garrity-software/gs-graph/LICENSE")
) )
val noPublishSettings = Seq(
publish := {}
)
val sharedSettings = Seq( val sharedSettings = Seq(
scalaVersion := scala3, scalaVersion := scala3,
version := semVerSelected.value, version := semVerSelected.value,
@ -21,6 +25,11 @@ val sharedSettings = Seq(
) )
val Deps = new { val Deps = new {
val Cats = new {
val Core: ModuleID = "org.typelevel" %% "cats-core" % "2.13.0"
}
val Gs = new { val Gs = new {
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.3.3" val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.3.3"
} }
@ -37,10 +46,21 @@ lazy val testSettings = Seq(
lazy val `gs-graph` = project lazy val `gs-graph` = project
.in(file(".")) .in(file("."))
.aggregate(core, cats)
.settings(sharedSettings) .settings(sharedSettings)
.settings(testSettings) .settings(testSettings)
.settings(name := s"${gsProjectName.value}-v${semVerMajor.value}") .settings(name := s"${gsProjectName.value}-v${semVerMajor.value}")
.settings(
libraryDependencies ++= Seq( lazy val core = project
) .in(file("modules/core"))
) .settings(sharedSettings)
.settings(testSettings)
.settings(name := s"${gsProjectName.value}-core-v${semVerMajor.value}")
lazy val cats = project
.in(file("modules/cats"))
.dependsOn(core)
.settings(sharedSettings)
.settings(testSettings)
.settings(name := s"${gsProjectName.value}-cats-v${semVerMajor.value}")
.settings(libraryDependencies ++= Seq(Deps.Cats.Core))

View file

@ -0,0 +1,188 @@
package gs.graph.v0.cats
import cats.Monad
import cats.syntax.all.*
import gs.graph.v0.Graph
import gs.graph.v0.Vertex
import gs.graph.v0.data.DataDigraph
import scala.collection.mutable.ListBuffer
import scala.collection.mutable.Stack
/** Graph traversal implementations (such as DFS and BFS) that operate in a
* monadic context, as defined by the `cats` library.
*/
object GraphTraversalCats:
/** Depth-first search (DFS) where the result of visiting each node is a
* side-effecting computation.
*
* This implementation selects the first [[Vertex]] as an arbitrary starting
* point.
*
* @param graph
* The [[Graph]] to traverse.
* @param visit
* The visitor function.
* @return
* The computation that describes the traversal.
*/
def dfsUnit[F[_]: Monad](
graph: Graph,
visit: Vertex => F[Unit]
): F[Unit] =
val output = ListBuffer.empty[F[Unit]]
val s = Stack.empty[Vertex]
val discovered = Array.fill(graph.numberOfVertices.value)(false)
val root = Vertex.Zero
s.push(root)
while !s.isEmpty
do
val v = s.pop()
if !discovered(v.ordinal) then
output.addOne(visit(v))
discovered(v.ordinal) = true
graph.neighbors(v).foreach(w => s.push(w))
else ()
output.foldLeft(Monad[F].unit) {
(
acc,
f
) => acc.flatMap(_ => f)
}
/** Depth-first search (DFS) where the result of visiting each node is some
* computed output.
*
* This implementation selects the first [[Vertex]] as an arbitrary starting
* point.
*
* @param graph
* The [[Graph]] to traverse.
* @param visit
* The visitor function.
* @return
* The computation that describes the traversal, producing a list of
* outputs.
*/
def dfs[F[_]: Monad, Output](
graph: Graph,
visit: Vertex => F[Output]
): F[List[Output]] =
val output = ListBuffer.empty[F[Output]]
val s = Stack.empty[Vertex]
val discovered = Array.fill(graph.numberOfVertices.value)(false)
val root = Vertex.Zero
s.push(root)
while !s.isEmpty
do
val v = s.pop()
if !discovered(v.ordinal) then
val _ = output.addOne(visit(v))
discovered(v.ordinal) = true
graph.neighbors(v).foreach(w => s.push(w))
else ()
output.foldLeft(Monad[F].pure(List.empty[Output])) {
(
acc,
result
) =>
for
current <- acc
out <- result
yield out :: current
}
/** Depth-first search (DFS) where the result of visiting each node is a
* side-effecting computation.
*
* This implementation performs DFS for _each root_ in the input
* [[DataDigraph]].
*
* @param graph
* The [[DataDigraph]] to traverse.
* @param visit
* The visitor function.
* @return
* The computation that describes the traversal.
*/
def dfsUnit[F[_]: Monad, A](
digraph: DataDigraph[A],
visit: (Vertex, A) => F[Unit]
): F[Unit] =
val output = ListBuffer.empty[F[Unit]]
val s = Stack.empty[Vertex]
val discovered = Array.fill(digraph.numberOfVertices.value)(false)
digraph.roots.foreach { root =>
s.push(root)
while !s.isEmpty
do
val v = s.pop()
if !discovered(v.ordinal) then
output.addOne(visit(v, digraph.data(v.ordinal)))
discovered(v.ordinal) = true
digraph.neighbors(v).foreach(w => s.push(w))
else ()
}
output.foldLeft(Monad[F].unit) {
(
acc,
f
) => acc.flatMap(_ => f)
}
/** Depth-first search (DFS) where the result of visiting each node is some
* computed output.
*
* This implementation performs DFS for _each root_ in the input
* [[DataDigraph]].
*
* @param graph
* The [[DataDigraph]] to traverse.
* @param visit
* The visitor function.
* @return
* The computation that describes the traversal, producing a list of
* outputs.
*/
def dfs[F[_]: Monad, A, Output](
digraph: DataDigraph[A],
visit: (Vertex, A) => F[Output]
): F[List[Output]] =
val output = ListBuffer.empty[F[Output]]
val s = Stack.empty[Vertex]
val discovered = Array.fill(digraph.numberOfVertices.value)(false)
digraph.roots.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)))
discovered(v.ordinal) = true
digraph.neighbors(v).foreach(w => s.push(w))
else ()
}
output.foldLeft(Monad[F].pure(List.empty[Output])) {
(
acc,
result
) =>
for
current <- acc
out <- result
yield out :: current
}
end GraphTraversalCats

View file

@ -0,0 +1,24 @@
package graph.gs.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)
def dfsUnit[F[_]: Monad](visit: Vertex => F[Unit]): F[Unit] =
GraphTraversalCats.dfsUnit[F](graph, visit)
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)

View file

@ -4,9 +4,9 @@ import gs.graph.v0.data.DataDigraph
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
import scala.collection.mutable.Stack import scala.collection.mutable.Stack
/** Depth-First Search (DFS) /** Graph traversal algorithms including DFS and BFS.
*/ */
object DFS: object GraphTraversal:
/** Depth-first search that executes a side-effecting function on each /** Depth-first search that executes a side-effecting function on each
* [[Vertex]]. This function will operate on _any_ [[Graph]]. * [[Vertex]]. This function will operate on _any_ [[Graph]].
@ -105,6 +105,8 @@ object DFS:
else () else ()
} }
()
/** Depth-first search that executes a function on each [[Vertex]] to produce /** Depth-first search that executes a function on each [[Vertex]] to produce
* some output, accepting the data stored for that [[Vertex]] as input. * some output, accepting the data stored for that [[Vertex]] as input.
* *
@ -141,4 +143,4 @@ object DFS:
output.toList output.toList
end DFS end GraphTraversal

View file

@ -0,0 +1,83 @@
package gs.graph.v0.data
import gs.graph.v0.Adjacency
import gs.graph.v0.GraphException
import gs.graph.v0.Size
import gs.graph.v0.Vertex
import gs.graph.v0.directed.Dag
/** Specialization of [[Dag]] that associates _data_ with _each [[Vertex]]_.
*/
final class DataDag[A] private (
val data: Vector[A],
n: Size,
a: Adjacency,
r: Vector[Vertex]
) extends Dag(n, a, r):
/** Retrieve the data associated with some [[Vertex]].
*
* This implementation throws an exception if the input [[Vertex]] is out of
* range for this graph.
*
* @param vertex
* The input [[Vertex]].
* @return
* The data associated with the input [[Vertex]].
*/
def dataAtUnsafe(vertex: Vertex): A =
if vertex < n then data.apply(vertex.ordinal)
else throw GraphException.VertexExceedsGraphBounds(vertex, n)
/** Retrieve the data associated with some [[Vertex]].
*
* @param vertex
* The input [[Vertex]].
* @return
* The data associated with the input [[Vertex]], or `None` if the
* [[Vertex]] is out of range for this graph.
*/
def dataAt(vertex: Vertex): Option[A] =
if vertex < n then Some(data.apply(vertex.ordinal)) else None
object DataDag:
/** @return
* An empty [[Dag]] with no data.
*/
def empty[A]: DataDag[A] =
new DataDag[A](
Vector.empty,
Size.Zero,
Adjacency.Empty,
Vector.empty
)
/** Instantiate a new [[DataDag]] given an input [[Dag]] and some data.
*
* Throws an exception if the input data vector does not exactly match the
* number of nodes in the graph -- each node MUST have a data element.
*
* @param dag
* The input [[Dag]].
* @param data
* The data to associate with the [[Dag]].
* @return
* The new [[DataDag]] instance.
*/
def fromDag[A](
dag: Dag,
data: Vector[A]
): DataDag[A] =
if dag.numberOfVertices != Size.fromVector(data) then
throw GraphException.DataGraphMismatch(
dag.numberOfVertices,
data.length
)
else
new DataDag(
data,
dag.numberOfVertices,
dag.adjacency,
dag.roots
)

View file

@ -6,7 +6,9 @@ import gs.graph.v0.Size
import gs.graph.v0.Vertex import gs.graph.v0.Vertex
import gs.graph.v0.directed.Digraph import gs.graph.v0.directed.Digraph
final class DataDigraph[A]( /** Specialization of [[Digraph]] that associates _data_ with _each [[Vertex]]_.
*/
final class DataDigraph[A] private (
val data: Vector[A], val data: Vector[A],
n: Size, n: Size,
a: Adjacency, a: Adjacency,
@ -40,6 +42,9 @@ final class DataDigraph[A](
object DataDigraph: object DataDigraph:
/** @return
* An empty [[Digraph]] with no data.
*/
def empty[A]: DataDigraph[A] = def empty[A]: DataDigraph[A] =
new DataDigraph[A]( new DataDigraph[A](
Vector.empty, Vector.empty,
@ -48,6 +53,19 @@ object DataDigraph:
Vector.empty Vector.empty
) )
/** Instantiate a new [[DataDigraph]] given an input [[Digraph]] and some
* data.
*
* Throws an exception if the input data vector does not exactly match the
* number of nodes in the graph -- each node MUST have a data element.
*
* @param digraph
* The input [[Digraph]].
* @param data
* The data to associate with the [[Digraph]].
* @return
* The new [[DataDigraph]] instance.
*/
def fromDigraph[A]( def fromDigraph[A](
digraph: Digraph, digraph: Digraph,
data: Vector[A] data: Vector[A]

View file

@ -1,17 +1,16 @@
package gs.graph.v0.dag package gs.graph.v0.directed
import gs.graph.v0.Adjacency import gs.graph.v0.Adjacency
import gs.graph.v0.GraphError import gs.graph.v0.GraphError
import gs.graph.v0.Size import gs.graph.v0.Size
import gs.graph.v0.Vertex import gs.graph.v0.Vertex
import gs.graph.v0.directed.Digraph
/** Directed Acyclic Graph (DAG): Represents a [[Digraph]] with no cycles. /** Directed Acyclic Graph (DAG): Represents a [[Digraph]] with no cycles.
* *
* Instances of this class guarantee that property - it can only be * Instances of this class guarantee that property - it can only be
* instantiated by validating some [[Digraph]]. * instantiated by validating some [[Digraph]].
*/ */
class Dag private ( class Dag protected (
n: Size, n: Size,
a: Adjacency, a: Adjacency,
r: Vector[Vertex] r: Vector[Vertex]

View file

@ -6,7 +6,6 @@ import gs.graph.v0.Graph
import gs.graph.v0.GraphDisposition import gs.graph.v0.GraphDisposition
import gs.graph.v0.Size import gs.graph.v0.Size
import gs.graph.v0.Vertex import gs.graph.v0.Vertex
import gs.graph.v0.dag.Dag
/** Directed implementation of [[Graph]]. /** Directed implementation of [[Graph]].
* *