From 468929f9e7ed20a1ee1b6e44d867d7463032ff13 Mon Sep 17 00:00:00 2001 From: Owen Lynch Date: Fri, 30 Aug 2024 15:59:35 -0700 Subject: [PATCH] draft of new design for catlog traits --- packages/catlog/src/lib.rs | 2 + packages/catlog/src/next/mod.rs | 2 + packages/catlog/src/next/one/category.rs | 174 ++++++++ packages/catlog/src/next/one/mod.rs | 3 + packages/catlog/src/next/one/path.rs | 412 ++++++++++++++++++ .../catlog/src/next/zero/generated_set.rs | 46 ++ packages/catlog/src/next/zero/graph.rs | 66 +++ packages/catlog/src/next/zero/mod.rs | 3 + packages/catlog/src/next/zero/set.rs | 85 ++++ 9 files changed, 793 insertions(+) create mode 100644 packages/catlog/src/next/mod.rs create mode 100644 packages/catlog/src/next/one/category.rs create mode 100644 packages/catlog/src/next/one/mod.rs create mode 100644 packages/catlog/src/next/one/path.rs create mode 100644 packages/catlog/src/next/zero/generated_set.rs create mode 100644 packages/catlog/src/next/zero/graph.rs create mode 100644 packages/catlog/src/next/zero/mod.rs create mode 100644 packages/catlog/src/next/zero/set.rs diff --git a/packages/catlog/src/lib.rs b/packages/catlog/src/lib.rs index cd1ab205..60756291 100644 --- a/packages/catlog/src/lib.rs +++ b/packages/catlog/src/lib.rs @@ -33,3 +33,5 @@ pub mod dbl; pub mod one; pub mod stdlib; pub mod zero; + +pub mod next; diff --git a/packages/catlog/src/next/mod.rs b/packages/catlog/src/next/mod.rs new file mode 100644 index 00000000..6ba9f45b --- /dev/null +++ b/packages/catlog/src/next/mod.rs @@ -0,0 +1,2 @@ +pub mod one; +pub mod zero; diff --git a/packages/catlog/src/next/one/category.rs b/packages/catlog/src/next/one/category.rs new file mode 100644 index 00000000..38a7718c --- /dev/null +++ b/packages/catlog/src/next/one/category.rs @@ -0,0 +1,174 @@ +use derive_more::derive::From; +use ref_cast::RefCast; + +use super::super::zero::{generated_set::*, graph::*, set::*}; +use super::path::*; + +pub trait Category { + type Ob; + type Objects: Set; + + type Mor; + type Morphisms: Set; + + type Dom: Mapping; + type Cod: Mapping; + + fn objects(&self) -> &Self::Objects; + + fn morphisms(&self) -> &Self::Morphisms; + + fn dom(&self) -> &Self::Dom; + + fn cod(&self) -> &Self::Cod; + + fn id(&self, x: &Self::Ob) -> Self::Mor; + + /// TODO: make n-ary + fn compose(&self, f: &Self::Mor, g: &Self::Mor) -> Self::Mor; +} + +pub trait FgCategory: + Category< + Objects: GeneratedSet, + Morphisms: GeneratedSet, + Dom: GeneratedMapping, + Cod: GeneratedMapping, +> +{ +} + +pub struct FreeCategory(G); + +#[derive(RefCast, From)] +#[repr(transparent)] +pub struct PathsOf(G); + +impl Set for PathsOf +where + G::V: Eq, + G::E: Eq, +{ + type Elem = Path; + + fn contains(&self, x: &Self::Elem) -> bool { + x.contained_in(&self.0) + } +} + +impl GeneratedSet for PathsOf +where + G::V: Eq, + G::E: Eq + Clone, +{ + type Generator = G::E; + type Generators = G::Edges; + + fn generators(&self) -> &Self::Generators { + self.0.edges() + } + + fn generate(&self, e: &Self::Generator) -> Self::Elem { + Path::single(e.clone()) + } +} + +#[derive(RefCast, From)] +#[repr(transparent)] +pub struct PathDom(G); + +impl Mapping for PathDom +where + G::V: Clone, +{ + type Dom = Path; + type Cod = G::V; + + fn ap(&self, x: &Self::Dom) -> Self::Cod { + x.src(&self.0) + } +} + +impl GeneratedMapping for PathDom +where + G::V: Clone, +{ + type DomGenerator = G::E; + + fn apgen(&self, x: &Self::DomGenerator) -> Self::Cod { + self.0.src().ap(x) + } +} + +#[derive(RefCast, From)] +#[repr(transparent)] +pub struct PathCod(G); + +impl Mapping for PathCod +where + G::V: Clone, +{ + type Dom = Path; + type Cod = G::V; + + fn ap(&self, x: &Self::Dom) -> Self::Cod { + x.src(&self.0) + } +} + +impl GeneratedMapping for PathCod +where + G::V: Clone, +{ + type DomGenerator = G::E; + + fn apgen(&self, x: &Self::DomGenerator) -> Self::Cod { + self.0.tgt().ap(x) + } +} + +impl Category for FreeCategory +where + G::V: Eq + Clone, + G::E: Eq + Clone, +{ + type Ob = G::V; + type Objects = TautologicallyGenerated; + + type Mor = Path; + type Morphisms = PathsOf; + + type Dom = PathDom; + type Cod = PathCod; + + fn objects(&self) -> &Self::Objects { + TautologicallyGenerated::ref_cast(self.0.vertices()) + } + + fn morphisms(&self) -> &Self::Morphisms { + PathsOf::ref_cast(&self.0) + } + + fn dom(&self) -> &Self::Dom { + PathDom::ref_cast(&self.0) + } + + fn cod(&self) -> &Self::Cod { + PathCod::ref_cast(&self.0) + } + + fn id(&self, x: &Self::Ob) -> Self::Mor { + Path::empty(x.clone()) + } + + fn compose(&self, f: &Self::Mor, g: &Self::Mor) -> Self::Mor { + Path::pair(f.clone(), g.clone()).flatten() + } +} + +impl FgCategory for FreeCategory +where + G::V: Eq + Clone, + G::E: Eq + Clone, +{ +} diff --git a/packages/catlog/src/next/one/mod.rs b/packages/catlog/src/next/one/mod.rs new file mode 100644 index 00000000..b965acc8 --- /dev/null +++ b/packages/catlog/src/next/one/mod.rs @@ -0,0 +1,3 @@ +pub mod path; + +pub mod category; diff --git a/packages/catlog/src/next/one/path.rs b/packages/catlog/src/next/one/path.rs new file mode 100644 index 00000000..1311792c --- /dev/null +++ b/packages/catlog/src/next/one/path.rs @@ -0,0 +1,412 @@ +/*! Paths in graphs and categories. + +The central data type is [`Path`]. In addition, this module provides a simple +data type for [path equations](`PathEq`). +*/ + +use either::Either; +use nonempty::{nonempty, NonEmpty}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +#[cfg(feature = "serde-wasm")] +use tsify_next::Tsify; + +use super::super::zero::graph::Graph; +use super::super::zero::set::*; +use crate::validate; + +/** A path in a [graph](Graph) or [category](crate::one::category::Category). + +This definition by cases can be compared with the perhaps more obvious +definition: + +``` +struct Path { + start: V, + end: V, // Optional: more symmetric but also more redundant. + seq: Vec, +} +``` + +Not only does the single struct store redundant (hence possibly inconsistent) +information when the sequence of edges is nonempty, one will often need to do a +case analysis on the edge sequence anyway to determine whether, say, +[`fold`](std::iter::Iterator::fold) can be called or the result of +[`reduce`](std::iter::Iterator::reduce) is valid. Thus, it seems better to reify +the two cases in the data structure itself. +*/ +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(tag = "tag", content = "content"))] +#[cfg_attr(feature = "serde-wasm", derive(Tsify))] +#[cfg_attr(feature = "serde-wasm", tsify(into_wasm_abi, from_wasm_abi))] +pub enum Path { + /// The identity, or empty, path at a vertex. + Id(V), + + /// A nontrivial path, comprising a *non-empty* vector of consecutive edges. + Seq(NonEmpty), +} + +impl From for Path { + fn from(e: E) -> Self { + Path::single(e) + } +} + +impl Path { + /// Constructs the empty or identity path. + pub fn empty(v: V) -> Self { + Path::Id(v) + } + + /// Constructs a path with a single edge. + pub fn single(e: E) -> Self { + Path::Seq(NonEmpty::singleton(e)) + } + + /// Constructs a pair of consecutive edges, or path of length 2. + pub fn pair(e: E, f: E) -> Self { + Path::Seq(nonempty![e, f]) + } + + /** Constructs a path from an iterator over edges. + + Returns `None` if the iterator is empty. + */ + pub fn collect(iter: I) -> Option + where + I: IntoIterator, + { + NonEmpty::collect(iter).map(Path::Seq) + } + + /** Constructs a path from a vector of edges. + + Returns `None` if the vector is empty. + */ + pub fn from_vec(vec: Vec) -> Option { + NonEmpty::from_vec(vec).map(Path::Seq) + } + + /// Length of the path. + pub fn len(&self) -> usize { + match self { + Path::Id(_) => 0, + Path::Seq(edges) => edges.len(), + } + } + + /// Is the path empty? + pub fn is_empty(&self) -> bool { + match self { + Path::Id(_) => true, + Path::Seq(_) => false, + } + } + + /** Iterates over edges in the path, if any. + + This method is a one-sided inverse to [`Path::collect`]. + */ + pub fn iter(&self) -> impl Iterator { + match self { + Path::Id(_) => Either::Left(std::iter::empty()), + Path::Seq(edges) => Either::Right(edges.iter()), + } + } + + /** Returns the unique edge in a path of length 1. + + This method is a one-sided inverse to [`Path::single`]. + */ + pub fn only(self) -> Option { + match self { + Path::Id(_) => None, + Path::Seq(edges) => { + if edges.tail.is_empty() { + Some(edges.head) + } else { + None + } + } + } + } + + /** Source of the path in the given graph. + + Assumes that the path is [contained in](Path::contained_in) the graph. + */ + pub fn src(&self, graph: &G) -> V + where + V: Clone, + G: Graph, + { + match self { + Path::Id(v) => v.clone(), + Path::Seq(edges) => graph.src().ap(edges.first()), + } + } + + /** Target of the path in the given graph. + + Assumes that the path is [contained in](Path::contained_in) the graph. + */ + pub fn tgt(&self, graph: &G) -> V + where + V: Clone, + G: Graph, + { + match self { + Path::Id(v) => v.clone(), + Path::Seq(edges) => graph.tgt().ap(edges.last()), + } + } + + /// Is the path contained in the given graph? + pub fn contained_in(&self, graph: &G) -> bool + where + V: Eq, + G: Graph, + { + match self { + Path::Id(v) => graph.vertices().contains(v), + Path::Seq(edges) => { + // All the edges are exist in the graph... + edges.iter().all(|e| graph.edges().contains(e)) && + // ...and their sources and target are compatible. Too strict? + std::iter::zip(edges.iter(), edges.iter().skip(1)).all( + |(e,f)| graph.tgt().ap(e) == graph.src().ap(f)) + } + } + } + + /// Reduces a path using functions on vertices and edges. + pub fn reduce(self, fv: FnV, fe: FnE) -> E + where + FnV: FnOnce(V) -> E, + FnE: FnMut(E, E) -> E, + { + match self { + Path::Id(v) => fv(v), + // `reduce` cannot fail since edge sequence is nonempty. + Path::Seq(edges) => edges.into_iter().reduce(fe).unwrap(), + } + } + + /// Maps a path over functions on vertices and edges. + pub fn map(self, fv: FnV, fe: FnE) -> Path + where + FnV: FnOnce(V) -> CodV, + FnE: FnMut(E) -> CodE, + { + match self { + Path::Id(v) => Path::Id(fv(v)), + Path::Seq(edges) => Path::Seq(edges.map(fe)), + } + } + + /// Maps a path over partial functions on vertices and edges. + pub fn partial_map(self, fv: FnV, fe: FnE) -> Option> + where + FnV: FnOnce(V) -> Option, + FnE: FnMut(E) -> Option, + { + match self { + Path::Id(v) => { + let w = fv(v)?; + Some(Path::Id(w)) + } + Path::Seq(edges) => { + let edges: Option> = edges.into_iter().map(fe).collect(); + let edges = edges?; + Path::from_vec(edges) + } + } + } + + /// Maps a path over fallible functions on vertices and edges. + pub fn try_map( + self, + fv: FnV, + fe: FnE, + ) -> Result, Err> + where + FnV: FnOnce(V) -> Result, + FnE: FnMut(E) -> Result, + { + match self { + Path::Id(v) => { + let w = fv(v)?; + Ok(Path::Id(w)) + } + Path::Seq(edges) => { + let edges: Result, _> = edges.into_iter().map(fe).collect(); + let edges = edges?; + Ok(Path::from_vec(edges).unwrap()) + } + } + } +} + +impl Path> { + /// Flatten a path of paths into a single path. + pub fn flatten(self) -> Path { + match self { + Path::Id(x) => Path::Id(x), + Path::Seq(fs) => { + if fs.iter().any(|p| matches!(p, Path::Seq(_))) { + let seqs = NonEmpty::collect(fs.into_iter().filter_map(|p| match p { + Path::Id(_) => None, + Path::Seq(gs) => Some(gs), + })); + Path::Seq(NonEmpty::flatten(seqs.unwrap())) + } else { + fs.head // An identity. + } + } + } + } +} + +/// A path in a graph with skeletal vertex and edge sets. +pub type SkelPath = Path; + +/// Assertion of an equation between the composites of two paths in a category. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PathEq { + /// Left hand side of equation. + pub lhs: Path, + + /// Right hand side of equation. + pub rhs: Path, +} + +impl PathEq { + /// Constructs a path equation with the given left- and right-hand sides. + pub fn new(lhs: Path, rhs: Path) -> PathEq { + PathEq { lhs, rhs } + } + + /** Source of the path equation in the given graph. + + Only well defined when the path equation is valid. + */ + pub fn src(&self, graph: &G) -> V + where + V: Clone, + G: Graph, + { + self.lhs.src(graph) // == self.rhs.src(graph) + } + + /** Target of the path equation in the given graph. + + Only well defined when the path equation is valid. + */ + pub fn tgt(&self, graph: &G) -> V + where + V: Clone, + G: Graph, + { + self.lhs.tgt(graph) // == self.rhs.tgt(graph) + } + + /// Validates that the path equation is well defined in the given graph. + pub fn validate_in(&self, graph: &G) -> Result<(), NonEmpty> + where + V: Eq + Clone, + G: Graph, + { + validate::wrap_errors(self.iter_invalid_in(graph)) + } + + /// Iterators over failures of the path equation to be well defined. + pub fn iter_invalid_in(&self, graph: &G) -> impl Iterator + where + V: Eq + Clone, + G: Graph, + { + let mut errs = Vec::new(); + if !self.lhs.contained_in(graph) { + errs.push(InvalidPathEq::Lhs()); + } + if !self.rhs.contained_in(graph) { + errs.push(InvalidPathEq::Rhs()); + } + if errs.is_empty() { + if self.lhs.src(graph) != self.rhs.src(graph) { + errs.push(InvalidPathEq::Src()); + } + if self.lhs.tgt(graph) != self.rhs.tgt(graph) { + errs.push(InvalidPathEq::Tgt()); + } + } + errs.into_iter() + } +} + +/// A failure of a path equation to be well defined in a graph. +#[derive(Debug)] +pub enum InvalidPathEq { + /// Path in left hand side of equation not contained in the graph. + Lhs(), + + /// Path in right hand side of equation not contained in the graph. + Rhs(), + + /// Sources of left and right hand sides of path equation are not equal. + Src(), + + /// Targets of left and right hand sides of path equation are not equal. + Tgt(), +} + +// #[cfg(test)] +// mod tests { +// use super::super::graph::SkelGraph; +// use super::*; +// use std::convert::identity; + +// #[test] +// fn path_in_graph() { +// let g = SkelGraph::triangle(); +// assert!(Path::Id(2).contained_in(&g)); +// assert!(!Path::Id(3).contained_in(&g)); +// assert!(Path::pair(0, 1).contained_in(&g)); +// assert!(!Path::pair(1, 0).contained_in(&g)); + +// let path = Path::pair(0, 1); +// assert_eq!(path.src(&g), 0); +// assert_eq!(path.tgt(&g), 2); +// } + +// #[test] +// fn singleton_path() { +// let e = 1; +// assert_eq!(SkelPath::single(e).only(), Some(e)); +// } + +// #[test] +// fn map_path() { +// let id = SkelPath::Id(1); +// assert_eq!(id.iter().count(), 0); +// assert_eq!(id.clone().map(|v| v + 1, identity), Path::Id(2)); +// assert_eq!(id.partial_map(|v| Some(v + 1), Some), Some(Path::Id(2))); + +// let pair = SkelPath::pair(0, 1); +// assert_eq!(pair.iter().count(), 2); +// assert_eq!(pair.clone().map(identity, |e| e + 1), Path::pair(1, 2)); +// assert_eq!(pair.partial_map(Some, |e| Some(e + 1)), Some(Path::pair(1, 2))); +// } + +// #[test] +// fn path_eq() { +// let g = SkelGraph::triangle(); +// let eq = PathEq::new(Path::pair(0, 1), Path::single(2)); +// assert_eq!(eq.src(&g), 0); +// assert_eq!(eq.tgt(&g), 2); +// assert!(eq.validate_in(&g).is_ok()); +// } +// } diff --git a/packages/catlog/src/next/zero/generated_set.rs b/packages/catlog/src/next/zero/generated_set.rs new file mode 100644 index 00000000..13a28d0d --- /dev/null +++ b/packages/catlog/src/next/zero/generated_set.rs @@ -0,0 +1,46 @@ +use derive_more::derive::From; +use ref_cast::RefCast; + +use super::set::*; + +pub trait GeneratedSet: Set { + type Generator; + type Generators: FinSet; + + fn generators(&self) -> &Self::Generators; + fn generate(&self, x: &Self::Generator) -> Self::Elem; +} + +pub trait GeneratedMapping: Mapping { + type DomGenerator; + + fn apgen(&self, x: &Self::DomGenerator) -> Self::Cod; +} + +#[derive(RefCast, From)] +#[repr(transparent)] +pub struct TautologicallyGenerated(S); + +impl Set for TautologicallyGenerated { + type Elem = S::Elem; + + fn contains(&self, x: &Self::Elem) -> bool { + self.0.contains(x) + } +} + +impl GeneratedSet for TautologicallyGenerated +where + S::Elem: Clone, +{ + type Generator = S::Elem; + type Generators = S; + + fn generators(&self) -> &Self::Generators { + &self.0 + } + + fn generate(&self, x: &Self::Generator) -> Self::Elem { + x.clone() + } +} diff --git a/packages/catlog/src/next/zero/graph.rs b/packages/catlog/src/next/zero/graph.rs new file mode 100644 index 00000000..6fecd1dc --- /dev/null +++ b/packages/catlog/src/next/zero/graph.rs @@ -0,0 +1,66 @@ +use super::set::FinSet; +use super::set::*; + +pub trait Graph { + type V; + type Vertices: Set; + + type E; + type Edges: Set; + + type Src: Mapping; + type Tgt: Mapping; + + fn vertices(&self) -> &Self::Vertices; + + fn edges(&self) -> &Self::Edges; + + fn src(&self) -> &Self::Src; + + fn tgt(&self) -> &Self::Tgt; +} + +pub trait FinGraph: + Graph +{ +} + +struct SkelFinGraph { + vertices: SkelFinSet, + edges: SkelFinSet, + src: SkelColumn, + tgt: SkelColumn, +} + +impl Graph for SkelFinGraph { + type V = usize; + type Vertices = SkelFinSet; + + type E = usize; + type Edges = SkelFinSet; + + type Src = SkelColumn; + type Tgt = SkelColumn; + + fn vertices(&self) -> &Self::Vertices { + &self.vertices + } + + fn edges(&self) -> &Self::Edges { + &self.edges + } + + fn src(&self) -> &Self::Src { + &self.src + } + + fn tgt(&self) -> &Self::Tgt { + &self.tgt + } +} + +impl FinGraph for SkelFinGraph {} + +fn incoming_edges(g: &G, v: &G::V) -> Vec { + g.tgt().fiber(v).collect() +} diff --git a/packages/catlog/src/next/zero/mod.rs b/packages/catlog/src/next/zero/mod.rs new file mode 100644 index 00000000..ea06425c --- /dev/null +++ b/packages/catlog/src/next/zero/mod.rs @@ -0,0 +1,3 @@ +pub mod generated_set; +pub mod graph; +pub mod set; diff --git a/packages/catlog/src/next/zero/set.rs b/packages/catlog/src/next/zero/set.rs new file mode 100644 index 00000000..e8b5a19a --- /dev/null +++ b/packages/catlog/src/next/zero/set.rs @@ -0,0 +1,85 @@ +use std::{collections::HashSet, hash::Hash}; + +pub trait Set { + type Elem; + + fn contains(&self, x: &Self::Elem) -> bool; +} + +pub trait FinSet: Set { + fn iter(&self) -> impl Iterator; + + fn len(&self) -> usize { + self.iter().count() + } + + fn isempty(&self) -> bool { + self.len() == 0 + } +} + +pub struct SkelFinSet(usize); + +impl Set for SkelFinSet { + type Elem = usize; + + fn contains(&self, x: &usize) -> bool { + *x < self.0 + } +} + +impl FinSet for SkelFinSet { + fn iter(&self) -> impl Iterator { + 0..self.0 + } +} + +struct HashFinSet(HashSet); + +impl Set for HashFinSet +where + A: Hash + Eq, +{ + type Elem = A; + + fn contains(&self, x: &A) -> bool { + self.0.contains(x) + } +} + +impl FinSet for HashFinSet +where + A: Hash + Eq + Clone, +{ + fn iter(&self) -> impl Iterator { + self.0.iter().map(|x| x.clone()) + } +} + +pub trait Mapping { + type Dom; + type Cod; + + fn ap(&self, x: &Self::Dom) -> Self::Cod; +} + +pub trait FinMapping: Mapping { + fn fiber(&self, y: &Self::Cod) -> impl Iterator; +} + +pub struct SkelColumn(Vec); + +impl Mapping for SkelColumn { + type Dom = usize; + type Cod = usize; + + fn ap(&self, x: &usize) -> usize { + self.0[*x] + } +} + +impl FinMapping for SkelColumn { + fn fiber(&self, y: &usize) -> impl Iterator { + self.0.iter().enumerate().filter(|(_, y2)| **y2 == *y).map(|(x, _)| x) + } +}