Refactoring to support cats. Adding documentation. Moving DAGs.
All checks were successful
/ Build and Release Library (push) Successful in 1m20s
All checks were successful
/ Build and Release Library (push) Successful in 1m20s
This commit is contained in:
parent
7b1c110c75
commit
2dcb9f7c26
18 changed files with 345 additions and 12 deletions
28
build.sbt
28
build.sbt
|
|
@ -13,6 +13,10 @@ ThisBuild / licenses := Seq(
|
|||
"MIT" -> url("https://git.garrity.co/garrity-software/gs-graph/LICENSE")
|
||||
)
|
||||
|
||||
val noPublishSettings = Seq(
|
||||
publish := {}
|
||||
)
|
||||
|
||||
val sharedSettings = Seq(
|
||||
scalaVersion := scala3,
|
||||
version := semVerSelected.value,
|
||||
|
|
@ -21,6 +25,11 @@ val sharedSettings = Seq(
|
|||
)
|
||||
|
||||
val Deps = new {
|
||||
|
||||
val Cats = new {
|
||||
val Core: ModuleID = "org.typelevel" %% "cats-core" % "2.13.0"
|
||||
}
|
||||
|
||||
val Gs = new {
|
||||
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.3.3"
|
||||
}
|
||||
|
|
@ -37,10 +46,21 @@ lazy val testSettings = Seq(
|
|||
|
||||
lazy val `gs-graph` = project
|
||||
.in(file("."))
|
||||
.aggregate(core, cats)
|
||||
.settings(sharedSettings)
|
||||
.settings(testSettings)
|
||||
.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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -4,9 +4,9 @@ import gs.graph.v0.data.DataDigraph
|
|||
import scala.collection.mutable.ListBuffer
|
||||
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
|
||||
* [[Vertex]]. This function will operate on _any_ [[Graph]].
|
||||
|
|
@ -105,6 +105,8 @@ object DFS:
|
|||
else ()
|
||||
}
|
||||
|
||||
()
|
||||
|
||||
/** Depth-first search that executes a function on each [[Vertex]] to produce
|
||||
* some output, accepting the data stored for that [[Vertex]] as input.
|
||||
*
|
||||
|
|
@ -141,4 +143,4 @@ object DFS:
|
|||
|
||||
output.toList
|
||||
|
||||
end DFS
|
||||
end GraphTraversal
|
||||
83
modules/core/src/main/scala/gs/graph/v0/data/DataDag.scala
Normal file
83
modules/core/src/main/scala/gs/graph/v0/data/DataDag.scala
Normal 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
|
||||
)
|
||||
|
|
@ -6,7 +6,9 @@ import gs.graph.v0.Size
|
|||
import gs.graph.v0.Vertex
|
||||
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],
|
||||
n: Size,
|
||||
a: Adjacency,
|
||||
|
|
@ -40,6 +42,9 @@ final class DataDigraph[A](
|
|||
|
||||
object DataDigraph:
|
||||
|
||||
/** @return
|
||||
* An empty [[Digraph]] with no data.
|
||||
*/
|
||||
def empty[A]: DataDigraph[A] =
|
||||
new DataDigraph[A](
|
||||
Vector.empty,
|
||||
|
|
@ -48,6 +53,19 @@ object DataDigraph:
|
|||
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](
|
||||
digraph: Digraph,
|
||||
data: Vector[A]
|
||||
|
|
@ -1,17 +1,16 @@
|
|||
package gs.graph.v0.dag
|
||||
package gs.graph.v0.directed
|
||||
|
||||
import gs.graph.v0.Adjacency
|
||||
import gs.graph.v0.GraphError
|
||||
import gs.graph.v0.Size
|
||||
import gs.graph.v0.Vertex
|
||||
import gs.graph.v0.directed.Digraph
|
||||
|
||||
/** Directed Acyclic Graph (DAG): Represents a [[Digraph]] with no cycles.
|
||||
*
|
||||
* Instances of this class guarantee that property - it can only be
|
||||
* instantiated by validating some [[Digraph]].
|
||||
*/
|
||||
class Dag private (
|
||||
class Dag protected (
|
||||
n: Size,
|
||||
a: Adjacency,
|
||||
r: Vector[Vertex]
|
||||
|
|
@ -6,7 +6,6 @@ import gs.graph.v0.Graph
|
|||
import gs.graph.v0.GraphDisposition
|
||||
import gs.graph.v0.Size
|
||||
import gs.graph.v0.Vertex
|
||||
import gs.graph.v0.dag.Dag
|
||||
|
||||
/** Directed implementation of [[Graph]].
|
||||
*
|
||||
Loading…
Add table
Reference in a new issue