packages feed

algebraic-edge-graphs-0.1.1: src/EdgeGraph/HigherKinded/Class.hs

-----------------------------------------------------------------------------
-- |
-- Module     : EdgeGraph.HigherKinded.Class
-- Copyright  : (c) Jack Liell-Cock 2025-2026
-- License    : MIT (see the file LICENSE)
-- Maintainer : jackliellcock@gmail.com
-- Stability  : experimental
--
-- This module defines the core higher-kinded type class 'EdgeGraph' for
-- algebraic edge graphs, a few graph subclasses, and basic polymorphic graph
-- construction primitives. Functions that cannot be implemented fully
-- polymorphically and require the use of an intermediate data type are not
-- included. For example, to compute the number of edge in an 'EdgeGraph'
-- expression you will need to use a concrete data type, such as
-- "EdgeGraph.Fold".
--
-- See "EdgeGraph.Class" for alternative definitions where the core type
-- class is not higher-kinded and permits more instances.
-----------------------------------------------------------------------------
module EdgeGraph.HigherKinded.Class (
  -- * The core type class
  EdgeGraph (..), empty, edge, overlay,

  -- * Basic graph construction primitives
  edges, overlays, intos, pitss, tipss,

  -- * Comparisons
  isSubgraphOf,

  -- * Graph properties
  isEmpty, hasEdge, edgeCount, edgeList, edgeSet,
  edgeIntSet,

  -- * Standard families of graphs
  path, circuit, clique, biclique, flower, node, tree, forest, mesh, torus, deBruijn,

  -- * Graph transformation
  removeEdge, replaceEdge, mergeEdges, splitEdge,
  induce,

  -- * Graph composition
  box,

  -- * Conversion between graph data types
  ToEdgeGraph (..)

) where

import Control.Applicative (empty, (<|>))
import Control.Monad
import Data.Foldable
import Data.Tree

import qualified Data.IntSet as IntSet
import qualified Data.Set    as Set

{-|
The core higher-kinded type class for constructing algebraic edge graphs is
defined by introducing the 'into', 'pits' and 'tips' methods to the standard
'MonadPlus' class and reusing the following existing methods:

* The 'EdgeGraph.HigherKinded.Class.empty' method comes from the 'Control.Applicative.Alternative' class and
corresponds to the /empty graph/. This module simply re-exports it.

* The 'edge' graph construction primitive is an alias for 'pure' of the
'Applicative' type class.

* Graph 'overlay' is an alias for '<|>' of the 'Control.Applicative.Alternative' type class.

The 'EdgeGraph' type class is characterised by the following minimal set of
axioms. In equations we use the infix operators '(EdgeGraph.Class.+++)' for 'overlay',
'(EdgeGraph.Class.>+>)' for 'into', '(EdgeGraph.Class.<+>)' for 'pits', and '(EdgeGraph.Class.>+<)' for 'tips'.

    * 'overlay' is commutative, associative, and idempotent with 'EdgeGraph.HigherKinded.Class.empty' as
      the identity:

        > x +++ y         == y +++ x
        > x +++ (y +++ z) == (x +++ y) +++ z
        > x +++ x         == x
        > x +++ empty     == x

    * 'EdgeGraph.HigherKinded.Class.empty' is the identity for 'into', 'pits', and 'tips':

        > empty >+> x == x
        > x >+> empty == x
        > empty <+> x == x
        > x <+> empty == x
        > empty >+< x == x
        > x >+< empty == x

    * 'pits' and 'tips' are commutative:

        > x <+> y == y <+> x
        > x >+< y == y >+< x

    * Decomposition: for any two connect operators @f@ and @g@ (each being
      any of '(EdgeGraph.Class.>+>)', '(EdgeGraph.Class.<+>)', or '(EdgeGraph.Class.>+<)'):

        > f x (g y z) == f x y +++ f x z +++ g y z
        > g (f x y) z == f x y +++ g x z +++ g y z

    * Reflexivity on single edges:

        > edge a <+> edge a == edge a
        > edge a >+< edge a == edge a

    * Transitivity (for non-empty @a@):

        > a <+> b +++ a <+> c == a <+> (b <+> c)
        > b >+> a +++ a <+> c == b >+> (a <+> c)
        > a >+> b +++ a >+> c == a >+> (b <+> c)
        > a >+< b +++ a >+> c == (a >+< b) >+> c
        > b >+> a +++ c >+> a == (b >+< c) >+> a
        > a >+< b +++ a >+< c == a >+< (b >+< c)

The following useful theorems can be proved from the above set of axioms.

    * Associativity of all connect operators:

        > x >+> (y >+> z) == (x >+> y) >+> z
        > x <+> (y <+> z) == (x <+> y) <+> z
        > x >+< (y >+< z) == (x >+< y) >+< z

    * Distributivity over 'overlay':

        > x >+> (y +++ z) == x >+> y +++ x >+> z
        > x <+> (y +++ z) == x <+> y +++ x <+> z
        > x >+< (y +++ z) == x >+< y +++ x >+< z

    * Absorption and saturation for each connect operator (shown for 'into'):

        > x >+> y +++ x +++ y == x >+> y
        > x >+> x             == (x >+> x) >+> x

When specifying the time and memory complexity of graph algorithms, /n/ will
denote the number of edges in the graph, /m/ will denote the number of
nodes in the graph, and /s/ will denote the /size/ of the corresponding
'EdgeGraph' expression.
-}
class (Traversable g, MonadPlus g) => EdgeGraph g where
    -- | Connect two graphs sequentially (pits of left to tips of right).
    into :: g a -> g a -> g a
    -- | Connect two graphs at pits (where outgoing edges overlap).
    pits :: g a -> g a -> g a
    -- | Connect two graphs at tips (where incoming edges overlap).
    tips :: g a -> g a -> g a

-- | Construct the graph comprising a single edge. An alias for 'pure'.
edge :: EdgeGraph g => a -> g a
edge = pure

-- | Overlay two graphs. An alias for '<|>'.
overlay :: EdgeGraph g => g a -> g a -> g a
overlay = (<|>)

-- | Construct the graph comprising a given list of isolated edges.
-- Complexity: /O(L)/ time, memory and size, where /L/ is the length of the
-- given list.
--
-- @
-- edges []  == 'EdgeGraph.HigherKinded.Class.empty'
-- edges [x] == 'edge' x
-- @
edges :: EdgeGraph g => [a] -> g a
edges = overlays . map edge

-- | Overlay a given list of graphs.
-- Complexity: /O(L)/ time and memory, and /O(S)/ size, where /L/ is the length
-- of the given list, and /S/ is the sum of sizes of the graphs in the list.
--
-- @
-- overlays []    == 'EdgeGraph.HigherKinded.Class.empty'
-- overlays [x]   == x
-- overlays [x,y] == 'overlay' x y
-- @
overlays :: EdgeGraph g => [g a] -> g a
overlays = msum

-- | Connect (into) a given list of graphs.
-- Complexity: /O(L)/ time and memory, and /O(S)/ size, where /L/ is the length
-- of the given list, and /S/ is the sum of sizes of the graphs in the list.
--
-- @
-- intos []    == 'EdgeGraph.HigherKinded.Class.empty'
-- intos [x]   == x
-- intos [x,y] == 'into' x y
-- @
intos :: EdgeGraph g => [g a] -> g a
intos = foldr into empty

-- | Connect (pits) a given list of graphs.
pitss :: EdgeGraph g => [g a] -> g a
pitss = foldr pits empty

-- | Connect (tips) a given list of graphs.
tipss :: EdgeGraph g => [g a] -> g a
tipss = foldr tips empty

-- | The 'isSubgraphOf' function takes two graphs and returns 'True' if the
-- first graph is a /subgraph/ of the second. Here is the current implementation:
--
-- @
-- isSubgraphOf x y = 'overlay' x y == y
-- @
-- The complexity therefore depends on the complexity of equality testing of
-- a particular graph instance.
--
-- @
-- isSubgraphOf 'EdgeGraph.HigherKinded.Class.empty'    x             == True
-- isSubgraphOf ('edge' x) 'EdgeGraph.HigherKinded.Class.empty'         == False
-- isSubgraphOf x        ('overlay' x y) == True
-- @
isSubgraphOf :: (EdgeGraph g, Eq (g a)) => g a -> g a -> Bool
isSubgraphOf x y = overlay x y == y

-- | Check if a graph is empty. A convenient alias for 'null'.
-- Complexity: /O(s)/ time.
--
-- @
-- isEmpty 'EdgeGraph.HigherKinded.Class.empty'                   == True
-- isEmpty ('overlay' 'EdgeGraph.HigherKinded.Class.empty' 'EdgeGraph.HigherKinded.Class.empty')   == True
-- isEmpty ('edge' x)                == False
-- isEmpty ('removeEdge' x $ 'edge' x) == True
-- @
isEmpty :: EdgeGraph g => g a -> Bool
isEmpty = null

-- | Check if a graph contains a given edge. A convenient alias for 'elem'.
-- Complexity: /O(s)/ time.
--
-- @
-- hasEdge x 'EdgeGraph.HigherKinded.Class.empty'          == False
-- hasEdge x ('edge' x)       == True
-- hasEdge x . 'removeEdge' x == const False
-- @
hasEdge :: (Eq a, EdgeGraph g) => a -> g a -> Bool
hasEdge = elem

-- | The number of distinct edges in a graph.
-- Complexity: /O(s * log(n))/ time.
--
-- @
-- edgeCount 'EdgeGraph.HigherKinded.Class.empty'    == 0
-- edgeCount ('edge' x) == 1
-- edgeCount          == 'length' . 'edgeList'
-- @
edgeCount :: (Ord a, EdgeGraph g) => g a -> Int
edgeCount = length . edgeList

-- | The sorted list of distinct edges of a given graph.
-- Complexity: /O(s * log(n))/ time and /O(n)/ memory.
--
-- @
-- edgeList 'EdgeGraph.HigherKinded.Class.empty'    == []
-- edgeList ('edge' x) == [x]
-- @
edgeList :: (Ord a, EdgeGraph g) => g a -> [a]
edgeList = Set.toAscList . edgeSet

-- | The set of distinct edges of a given graph.
-- Complexity: /O(s * log(n))/ time and /O(n)/ memory.
--
-- @
-- edgeSet 'EdgeGraph.HigherKinded.Class.empty'  == 'Data.Set.empty'
-- edgeSet . 'edge' == 'Data.Set.singleton'
-- @
edgeSet :: (Ord a, EdgeGraph g) => g a -> Set.Set a
edgeSet = foldr Set.insert Set.empty

-- | The set of edges of a given graph, specialised for graphs with
-- edges of type 'Int'.
-- Complexity: /O(s * log(n))/ time and /O(n)/ memory.
--
-- @
-- edgeIntSet 'EdgeGraph.HigherKinded.Class.empty'  == 'Data.IntSet.empty'
-- edgeIntSet . 'edge' == 'Data.IntSet.singleton'
-- @
edgeIntSet :: EdgeGraph g => g Int -> IntSet.IntSet
edgeIntSet = foldr IntSet.insert IntSet.empty

-- | The /path/ on a list of edges, connecting consecutive edges via 'into'.
-- Complexity: /O(L)/ time, memory and size, where /L/ is the length of the
-- given list.
--
-- @
-- path []      == 'EdgeGraph.HigherKinded.Class.empty'
-- path [x]     == 'edge' x
-- path [x,y]   == 'into' ('edge' x) ('edge' y)
-- path [x,y,z] == 'overlays' ['into' ('edge' x) ('edge' y), 'into' ('edge' y) ('edge' z)]
-- @
path :: EdgeGraph g => [a] -> g a
path []  = empty
path [x] = edge x
path xs  = overlays $ zipWith (\a b -> into (edge a) (edge b)) xs (drop 1 xs)

-- | The /circuit/ on a list of edges, connecting consecutive edges via 'into'
-- in a cycle.
-- Complexity: /O(L)/ time, memory and size, where /L/ is the length of the
-- given list.
--
-- @
-- circuit []    == 'EdgeGraph.HigherKinded.Class.empty'
-- circuit [x]   == 'into' ('edge' x) ('edge' x)
-- circuit [x,y] == 'overlays' ['into' ('edge' x) ('edge' y), 'into' ('edge' y) ('edge' x)]
-- @
circuit :: EdgeGraph g => [a] -> g a
circuit []       = empty
circuit (x : xs) = overlays $ zipWith (\a b -> into (edge a) (edge b)) (x : xs) (xs ++ [x])

-- | The /clique/ on a list of edges (fully connected via 'into'). Each edge
-- connects to every later edge in the list, due to the decomposition axiom.
-- Complexity: /O(L)/ time, memory and size, where /L/ is the length of the
-- given list.
--
-- @
-- clique []    == 'EdgeGraph.HigherKinded.Class.empty'
-- clique [x]   == 'edge' x
-- clique [x,y] == 'into' ('edge' x) ('edge' y)
-- @
clique :: EdgeGraph g => [a] -> g a
clique = intos . map edge

-- | The /biclique/ on two lists of edges. Every edge in the first list
-- connects to every edge in the second list via 'into'.
-- Complexity: /O(L1 + L2)/ time, memory and size, where /L1/ and /L2/ are the
-- lengths of the given lists.
--
-- @
-- biclique []  []  == 'EdgeGraph.HigherKinded.Class.empty'
-- biclique [x] []  == 'edge' x
-- biclique []  [y] == 'edge' y
-- @
biclique :: EdgeGraph g => [a] -> [a] -> g a
biclique xs ys = into (edges xs) (edges ys)

-- | The /flower graph/ on a list of edges. All edges are fully connected via
-- 'into' in a loop, forming petal-like structures around a central node.
-- This is equivalent to a 'clique' where the first edge is repeated at the
-- end, creating a cycle through the 'into' operator.
-- Complexity: /O(L)/ time, memory and size, where /L/ is the length of the
-- given list.
--
-- @
-- flower []      == 'EdgeGraph.HigherKinded.Class.empty'
-- flower [x]     == 'into' ('edge' x) ('edge' x)
-- flower [x,y]   == 'intos' ['edge' x, 'edge' y, 'edge' x]
-- flower [x,y,z] == 'intos' ['edge' x, 'edge' y, 'edge' z, 'edge' x]
-- @
flower :: EdgeGraph g => [a] -> g a
flower []       = empty
flower (x : xs) = intos (map edge (x : xs ++ [x]))

-- | Construct a /node/ from a list of incoming edges and a list of outgoing
-- edges. The incoming edges share a common pit at the node, and the outgoing
-- edges share a common tip at the node. When one of the lists is empty, 'pits'
-- or 'tips' is used to ensure the remaining edges are still connected at the
-- node.
-- Complexity: /O(L1 + L2)/ time, memory and size, where /L1/ and /L2/ are the
-- lengths of the given lists.
--
-- @
-- node []    []    == 'EdgeGraph.HigherKinded.Class.empty'
-- node [x]   []    == 'edge' x
-- node []    [y]   == 'edge' y
-- node [x]   [y]   == 'into' ('edge' x) ('edge' y)
-- node [x]   [y,z] == 'into' ('edge' x) ('tips' ('edge' y) ('edge' z))
-- node [x,y] [z]   == 'into' ('pits' ('edge' x) ('edge' y)) ('edge' z)
-- @
node :: EdgeGraph g => [a] -> [a] -> g a
node xs ys = pitss (map edge xs) `into` tipss (map edge ys)

-- | The /tree graph/ constructed from a given 'Data.Tree.Tree' data structure.
-- Complexity: /O(T)/ time, memory and size, where /T/ is the size of the
-- given tree.
tree :: EdgeGraph g => Tree a -> g a
tree (Node x f) = overlay (into (edge x) (edges (map rootLabel f))) (forest f)

-- | The /forest graph/ constructed from a given 'Data.Tree.Forest' data structure.
-- Complexity: /O(F)/ time, memory and size, where /F/ is the size of the
-- given forest.
forest :: EdgeGraph g => Forest a -> g a
forest = overlays . map tree

-- | Construct a /mesh graph/ from two lists of edges.
-- Complexity: /O(L1 * L2)/ time, memory and size, where /L1/ and /L2/ are the
-- lengths of the given lists.
--
-- @
-- mesh xs  []  == 'EdgeGraph.HigherKinded.Class.empty'
-- mesh []  ys  == 'EdgeGraph.HigherKinded.Class.empty'
-- mesh [x] [y] == 'edge' (x, y)
-- @
mesh :: EdgeGraph g => [a] -> [b] -> g (a, b)
mesh xs ys = path xs `box` path ys

-- | Construct a /torus graph/ from two lists of edges.
-- Complexity: /O(L1 * L2)/ time, memory and size, where /L1/ and /L2/ are the
-- lengths of the given lists.
--
-- @
-- torus xs [] == 'EdgeGraph.HigherKinded.Class.empty'
-- torus [] ys == 'EdgeGraph.HigherKinded.Class.empty'
-- @
torus :: EdgeGraph g => [a] -> [b] -> g (a, b)
torus xs ys = circuit xs `box` circuit ys

-- | Construct a /De Bruijn graph/ of given dimension and symbols of a given
-- alphabet.
-- Complexity: /O(A * D^A)/ time, memory and size, where /A/ is the size of the
-- alphabet and /D/ is the dimension of the graph.
--
-- @
-- deBruijn k [] == 'EdgeGraph.HigherKinded.Class.empty'
-- @
deBruijn :: EdgeGraph g => Int -> [a] -> g [a]
deBruijn len alphabet = skeleton >>= expand
  where
    overlaps = mapM (const alphabet) [2..len]
    skeleton = overlays [ into (edge (Left s)) (edge (Right s)) | s <- overlaps ]
    expand v = edges [ either ([a] ++) (++ [a]) v | a <- alphabet ]

-- | Remove all occurrences of an edge from the graph.
-- Complexity: /O(s)/ time, memory and size.
--
-- @
-- removeEdge x ('edge' x)       == 'EdgeGraph.HigherKinded.Class.empty'
-- removeEdge x . removeEdge x == removeEdge x
-- @
removeEdge :: (Eq a, EdgeGraph g) => a -> g a -> g a
removeEdge v = induce (/= v)

-- | The function @replaceEdge x y@ replaces edge @x@ with edge
-- label @y@ in a given graph. If @y@ already exists, the labels will be merged.
-- Complexity: /O(s)/ time, memory and size.
--
-- @
-- replaceEdge x x          == id
-- replaceEdge x y ('edge' x) == 'edge' y
-- replaceEdge x y          == 'mergeEdges' (== x) y
-- @
replaceEdge :: (Eq a, EdgeGraph g) => a -> a -> g a -> g a
replaceEdge u v = fmap $ \w -> if w == u then v else w

-- | Merge edges satisfying a given predicate with a given edge.
-- Complexity: /O(s)/ time, memory and size, assuming that the predicate takes
-- /O(1)/ to be evaluated.
--
-- @
-- mergeEdges (const False) x == id
-- mergeEdges (== x)        y == 'replaceEdge' x y
-- @
mergeEdges :: (Eq a, EdgeGraph g) => (a -> Bool) -> a -> g a -> g a
mergeEdges p v = fmap $ \w -> if p w then v else w

-- | Split an edge into a list of edges with the same connectivity.
-- Complexity: /O(s + k * L)/ time, memory and size, where /k/ is the number of
-- occurrences of the edge in the expression and /L/ is the length of the
-- given list.
--
-- @
-- splitEdge x []  == 'removeEdge' x
-- splitEdge x [x] == id
-- splitEdge x [y] == 'replaceEdge' x y
-- @
splitEdge :: (Eq a, EdgeGraph g) => a -> [a] -> g a -> g a
splitEdge v us g = g >>= \w -> if w == v then edges us else edge w

-- | Construct the /induced subgraph/ of a given graph by removing edges
-- that do not satisfy a given predicate.
-- Complexity: /O(s)/ time, memory and size, assuming that the predicate takes
-- /O(1)/ to be evaluated.
--
-- @
-- induce (const True)  x == x
-- induce (const False) x == 'EdgeGraph.HigherKinded.Class.empty'
-- induce (/= x)          == 'removeEdge' x
-- induce p . induce q    == induce (\\x -> p x && q x)
-- @
induce :: EdgeGraph g => (a -> Bool) -> g a -> g a
induce = mfilter

-- | Compute the /Cartesian product/ of graphs.
-- Complexity: /O(s1 * s2)/ time, memory and size, where /s1/ and /s2/ are the
-- sizes of the given graphs.
--
-- @
-- box ('path' [0,1]) ('path' "ab") == 'edges' [ ((0,\'a\'),(0,\'b\')), ((0,\'a\'),(1,\'a\'))
--                                       , ((0,\'b\'),(1,\'b\')), ((1,\'a\'),(1,\'b\')) ]
-- @
-- Up to an isomorphism between the resulting edge types, this operation
-- is /commutative/, /associative/, /distributes/ over 'overlay', has singleton
-- graphs as /identities/ and 'EdgeGraph.HigherKinded.Class.empty' as the /annihilating zero/. Below @~~@
-- stands for the equality up to an isomorphism, e.g. @(x, ()) ~~ x@.
--
-- @
-- box x y             ~~ box y x
-- box x (box y z)     ~~ box (box x y) z
-- box x ('overlay' y z) == 'overlay' (box x y) (box x z)
-- box x ('edge' ())     ~~ x
-- box x 'EdgeGraph.HigherKinded.Class.empty'         ~~ 'EdgeGraph.HigherKinded.Class.empty'
-- @
box :: EdgeGraph g => g a -> g b -> g (a, b)
box x y = msum $ xs ++ ys
  where
    xs = map (\b -> fmap (,b) x) $ toList y
    ys = map (\a -> fmap (a,) y) $ toList x

-- | The 'ToEdgeGraph' type class captures data types that can be converted to
-- polymorphic edge graph expressions. The conversion method 'toEdgeGraph'
-- semantically acts as the identity on graph data structures, but allows to
-- convert graphs between different data representations.
class ToEdgeGraph t where
  toEdgeGraph :: EdgeGraph g => t a -> g a