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")
|
"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))
|
||||||
|
|
|
||||||
|
|
@ -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.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
|
||||||
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.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]
|
||||||
|
|
@ -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]
|
||||||
|
|
@ -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]].
|
||||||
*
|
*
|
||||||
Loading…
Add table
Reference in a new issue