Algebraic Path Finding
21 March 2023
There’s something magical about polymorphic algorithms. Really, isn’t it awesome to write a piece of code once and then just tweak the meaning of some inputs to solve another seemly unrelated problem? Let’s see an example of those with applications ranging from regular expressions to matrix inversion, with a lot of useful queries on graphs along the way.
Today we will continue our explorations in the land of dynamic programming but with a much more abstract view. As it stands, dynamic programming in a finite horizon isn’t necessarily concerned with solving optimization problems. It can be abstracted to any kind of problem where we have well-behaved notions of combining and aggregating values along paths, and the solution methods remain exactly the same. This way, we go from the optimization view of the shortest path problem to the generic view of the algebraic path problem, which subsumes a lot of classical algorithms from computational mathematics.
By the way, be alert! Because it’s possible that your favorite algorithm is also an alternative view on finding shortest paths.
{-# LANGUAGE PackageImports, DeriveTraversable #-}
{-# LANGUAGE DataKinds, KindSignatures #-}
{-# LANGUAGE TypeApplications, AllowAmbiguousTypes #-}
{-# LANGUAGE ScopedTypeVariables, RankNTypes #-}
import Control.Applicative (liftA2)
import Data.Ord (Down(..))
import Data.Semigroup (Min(..), Sum(..), Product(..))
import Data.List (foldl')
import Data.Proxy (Proxy(Proxy))
import GHC.TypeNats (KnownNat, Nat, natVal)
import "array" Data.Array (Array, array, Ix(range), (!), (//), bounds)
Classical Shortest Paths
Consider a graph with weights on its edges. Like the
one in the figure, for example. One way to represent
this graph is via an adjacency matrix A whose component A(s, t) is the weight of the
edge between nodes s
and t. Notice that
since there may not be edges between all vertices (after
all, what’s the fun in a complete graph?), A is a partial function. We
can solve this by adjoining a value \infty to the possible
weights and assigning it as the weight of the
non-existent edges. In case this solution seems like a
dirty trick to you, a possibility is to think of this
process as the same as returning a Maybe
value to make a partial function total, with \infty playing the role of
Nothing
.1
Given this graph, a common question to ask is what are best ways to travel between its vertices. That is, if you want to go from node s to node t, which sequence of edges should you choose? If you’ve read what I have been writing about dynamic programming, you are probably already smelling the sweet scent of value functions and Bellman equations in the air.
Let \mathcal{S} be the (finite) set of nodes in the graph, and let’s define V \colon \mathcal{S}\times \mathcal{S}\to (-\infty, \infty] as the value function which tells us the length of the shortest path between two vertices. With a bit of dynamic programming, we can find out that V satisfies a Bellman equation:
\begin{aligned} V(s, s) &= 0 \\ V(s, t) &= \min_{q \in \mathcal{S}}\, A(s, q) + V(q, t), \; \forall s \ne t. \end{aligned}
The first line is the base case for the recursion and represents the 0-step paths. It costs nothing to just stay where you are, but it is in turn impossible to reach any other vertex in zero steps. This may seem really simple, but let’s nevertheless define a matrix I to represent this 0-step reachability relation. Why we want to do that will soon be clear.
I(s, t) = \begin{cases} 0 ,& s = t \\ \infty ,& s \ne t. \end{cases}
We can use I to write the recurrence more compactly:
V(s, t) = \min \left\{ I(s, t),\, \min_{q \in \mathcal{S}} A(s, q) + V(q, t) \right\}
What the above equation means is that the minimal distance between two vertices is the shortest among staying in the vertex (an empty path) and recursively following the shortest path with at least one edge.
Every problem is linear if you squint hard enough
The equation above is indeed rather ugly. However, by looking at it with care, one can the structure unveiling. First, the term
\min_{q \in \mathcal{S}}\, A(s, q) + V(q, t)
looks a lot like some kind of composition where we aggregate over the middle index q. Even more: if you squint your eyes enough, it looks a lot like the formula for matrix multiplication.
Well, let’s try to discover where this idea takes us. Even if it is just for the sake of abstract nonsense, it could be interesting. In general, when talking about real numbers, we use the usual arithmetic structure with addition and multiplication. Today we’re going to be eccentric and define a new kind of addition and multiplication:
\begin{aligned} a \oplus b &= \min\{a,\, b\}, \\ a \otimes b &= a + b, \\ \mathbf{0} &= \infty, \\ \mathbf{1} &= 0. \end{aligned}
So, what is happening here? The thing is: the real numbers extended with \infty satisfy all the axioms for a Semiring structure if we take sum to be \min and product to be +. This is called the min-plus or Tropical semiring, and surprisingly satisfies a lot of the axioms we expect from a sum and product The main difference is that instead of having inverses, \oplus is idempotent.
Before further exploring the properties of the Tropical semiring, or of semirings in general, let’s take advantage of our new notation to rewrite the ugly equation
V(s, t) = I(s, t) \oplus \bigoplus_{q \in \mathcal{S}} A(s, q) \otimes V(q, t)
Into the much more elegant (and index-free!) matrix format
V = I \oplus (A \otimes V).
By now, the choice of the letter I for the 0-step becomes clear: it is the identity matrix in the Tropical algebra!
What is cool is that in this form, finding the shortest path becomes the same as solving a linear system. Can we transfer the methods from classical linear algebra to this semiring setting? Unfortunately the answer is in general negative… This is why we will have to require a further piece of structure from our semirings: a closure operator denoted ^* which returns the fixed point of the mapping x \mapsto \mathbf{1} \oplus a \otimes x, that is, we require that
x^* = \mathbf{1} \oplus a \otimes x^* = \mathbf{1} \oplus x^\star \otimes a.
Now, you may call me a cheater since this is the scalar version of the shortest path equation. Nevertheless, we will see that while such an operator tends to be pretty simple to define for the scalar field, it is also the missing piece for solving the Algebraic Path Problem. That is, an algorithm capable of solving the Bellman equation in a way that is polymorphic on the semiring.
Alright, that was a pretty mathematical introduction.
It’s far from time for this post to become more
computational. Let’s take the above discussion to
Haskell land by defining a Semiring
class.
class Semiring r where
one :: r
zero,|+|), (|*|) :: r -> r -> r
( closure :: r -> r
infixl 6 |+|
infixl 7 |*|
infixr 8 |^|
For later use, let’s also define an exponentiation operator using the classic divide-and-conquer formula.
(|^|) :: (Semiring r, Integral n) => r -> n -> r
|^| 0 = one
x |^| n | even n = x |^| div n 2 |*| x |^| div n 2
x | otherwise = x |*| x |^| (n-1)
As usual, there are also a bunch of laws that an element of this class must obey. Since those are all pretty standard, I will just direct you to the Wikipedia article on the topic.
The Tropical Semiring and Shortest Paths
One can think of a semiring as a structure with an operation for combining two values (\otimes) and another for aggregating values (\oplus). Since the min-plus semiring was our choice for intuition, let’s start the implementation with it.
data Tropical a = Finite a | Infinity
deriving (Eq, Functor)
One thing to notice about our motivation is that we only used two operations on the real numbers: the capacity of combining weights along a path with addition (our product) and the capacity of aggregating weights of different paths by choosing the smallest among them (our addition). Thus, the Tropical structure works for any monoid endowed with a total order. We will define the operations in this more general setting2 however, I advise you to keep the Real numbers in mind for the intuition. You will shortly see why this generality pays off.
instance (Ord a, Monoid a) => Semiring (Tropical a) where
= Infinity -- Weight of nonexistent path
zero = Finite mempty -- Weight of empty path one
For addition, Infinity
works as identity
since we are minimizing.
Infinity |+| x = x
|+| Infinity = x
x Finite x |+| Finite y = Finite (min x y) -- Choose the smallest among paths
For the product, we combine two values in the usual
way with the extension that Infinity
is
absorbing: adding an infinite quantity to any number, no
matter how small, always produces an infinite
result.
|*| Infinity = Infinity
x Infinity |*| y = Infinity
Finite x |*| Finite y = Finite (x <> y) -- Combine weights along a path
Now, this next one is interesting. The closure of
x
is equivalent to the shortest distance in
a graph with a single node. If there are no negative
cycles, this should equal the weight of an empty path,
which is the identity to the product.
= one -- the distance taken by not moving closure _
Of course, we are not interested only in single node graphs. Let’s take a look at weighted adjacency matrices.
Matrices over a semiring are a semiring
Since working with Haskell arrays could be described as anything but pleasant, let’s define some simple wrappers to ease our life a little bit. We will go with some dependentish square matrices, but nothing that makes our code too complicated.
type Edge = (Nat, Nat)
-- | n x n square matrix with components in r
newtype Matrix (n :: Nat) a = Matrix (Array Edge a)
deriving (Eq, Functor, Foldable, Traversable)
To ease our life, let’s also define some methods to
get a cleaner interface. First of all, a smart
constructor that transforms a function on pairs of
indices into a Matrix
.
-- | Extract a type-level natural to the term level
nat :: forall n. KnownNat n => Nat
= natVal (Proxy @n)
nat
matrix :: forall n a. KnownNat n => (Edge -> a) -> Matrix n a
= Matrix $ array ends [(x, f x) | x <- range ends]
matrix f where ends = ((1, 1), (nat @n, nat @n))
And another smart constructor that turns a list of edges into the respective adjacency matrix.
adjacency :: forall n r. (KnownNat n, Semiring r)
=> [(Edge, r)] -> Matrix n r
= let Matrix o = one :: Matrix n r
adjacency es in Matrix (o // es)
It is also nice to have an Applicative
instance, in order to aide us during future lifts.
Notice that since our matrices are a type with fixed
size, we can lift operations by simply matching the
components.
instance KnownNat n => Applicative (Matrix n) where
pure x = matrix (const x)
Matrix f) <*> (Matrix y) = matrix $ \(s,t) -> (f ! (s,t)) (y ! (s,t)) (
With these tools, we can keep away from the Array
low-level API, with the exception of indexing. Now we
can properly define the Semiring
instance
for a matrix.
Addition and zero are simply defined pointwisely.
instance (KnownNat n, Semiring r) => Semiring (Matrix n r) where
-- These are the pointwise lifts of their scalar versions
= pure zero
zero |+|) = liftA2 (|+|) (
For multiplication, we define one
as the
identity matrix with one
s on the diagonal
and zero elsewhere (the same as in the introduction),
and use the traditional definition to build the
product.
-- Identity matrix
= matrix $ \(i, j) -> if i == j then one else zero
one -- Matrix multiplication using Semiring operators
Matrix x |*| Matrix y = matrix contract
where
= add [x ! (s, q) |*| y ! (q, t) | q <- [1..nat @n]]
contract (s, t) = foldl' (|+|) zero add
Finally, it is time for our main algorithm: how to calculate the closure of a matrix. Let’s take another look at the equation that A^\star must obey in order to gather some intuition.
A^\star = I \oplus (A \otimes A^\star)
Let’s try an experiment: what happens if we repeatedly substitute the A^\star term on the right-hand side with the entire right-hand side? After all, they’re equal… By doing that, we get a representation of the closure as a power series.
\begin{aligned} A^\star &= I \oplus A \oplus A^2 \oplus A^3 \oplus A^4 \oplus \ldots \\ &= \bigoplus_{k = 1}^\infty A^k \end{aligned}
What does this view contributes to us? Let’s take a look at the Tropical Semiring. Each power A^k of the adjacency matrix has at its components A^k(s, t) the distance of the shortest path from s to t with exactly k edges. Thus, while the recursive Bellman equation decomposes the cost of an edge plus the cost of a smaller path, this power series formulation, on the other hand, is saying that the optimal cost among all paths may be decomposed by path length. The minimum among all path is the minimum among all paths of any fixed length.3
Since there are just n vertices and we are assuming no negative cycles in order for the problem to be well-posed, looking at any power A^{n+1} is redundant. Because, without negative cycles, it is never worth it to pass through any vertex more than once.
A way to calculate the closure is with a variation the Floyd-Warshall or the Gauss-Jordan algorithm that is adapted to work on any closed semiring.4
= one |+| foldl' step x [1..nat @n]
closure x where step (Matrix m) k = matrix relax
where relax (i, j) = let c = closure (m ! (k, k))
in m ! (i, j) |+| m ! (i, k) |*| c |*| m ! (k, j)
To see that it is in general necessary to cross n vertices, consider a graph consisting of a single path passing through all nodes. It takes exactly n iterations to discover that all vertices are reachable from the first.
Everything you ever wanted to know about your graphs
Great, we have all the necessary tools to solve the
Bellman Equation on the tropical semiring! Let’s write a
little wrapper that turns an association list of
weighted edges into the matrix of shortest distance
between vertices. It works by converting the weights
into a Tropical semiring where we use Haskell’s
Sum
monoid to indicate that it should
combine values with addition.
shortestDistances :: (KnownNat n, Ord r, Num r)
=> [(Edge, r)] -> Matrix n (Tropical (Sum r))
= closure . adjacency . fmap tropicalize
shortestDistances where tropicalize = fmap (Finite . Sum)
This matrix represents the problem’s optimal value function, and, from it we can reconstruct the optimal paths. But wouldn’t it be cooler if the algorithm could already form the paths for us? It is, in some sense, walking over the graph after all… As you may guess, there is a semiring capable of just that!
Thanks to our generic Tropical semiring and Haskell’s default instances, this addition is almost an one-liner.
-- | Perform an optimization problem storing the optimal paths
solveWithPath :: (KnownNat n, Ord (f r), Monoid (f r))
=> (a -> f r) -> [(Edge, a)] -> Matrix n (Tropical (f r, [Edge]))
= closure . adjacency . fmap withEdge
solveWithPath f where withEdge (e, w) = (e, Finite (f w, [e]))
shortestPath :: (KnownNat n, Ord r, Num r)
=> [(Edge, r)] -> Matrix n (Tropical (Sum r, [Edge]))
= solveWithPath Sum shortestPath
Well, how does the above work? The List type has a
default Monoid instance given by []
and
concatenation, and pairs of monoids are also monoids
with pointwise operations. Furthermore, Haskell provides
default Òrd
instances for pairs and lists
using the lexicographic order. Thus, in the type
Tropical (Sum r, [Edge])
, the product sums
the weights in the first component and concatenate the
paths in the second, while the addition takes the
lexicographic minimum: keep the pair with smallest
weight or, if they are equal, disambiguate the paths
lexicographically. Since we only want one shortest path,
any disambiguation criterion works, and we happily
receive our path.
Of course, this technique works for many more
operations. By using the Down
constructor,
which inverts an Ord
instance, we can also
solve maximization problems! For example, by changing a
single constructor, we can query for the largest
path.
largestPath :: (KnownNat n, Ord r, Num r)
=> [(Edge, r)] -> Matrix n (Tropical (Down (Sum r), [Edge]))
= solveWithPath (Down . Sum) largestPath
Keep in mind that in this case, our previous assumption of no negative cycles becomes the dual, but for some reason much less agreeable, assumption of positive cycles.
Now suppose that you heavy a very important letter to send to somebody you know. Unfortunately, the roads (or the network links) are each day more dangerous and finding a route that will actually deliver your message is no easy task. One can model this situation as a graph whose weights this edge’s reliability, that is, the probability of the message crossing that edge without being compromised. Assuming independence, the reliability of a path is the product of its edge’s probabilities and we can find the most reliable path by maximizing among all of them. At this point, you can already guess: there is a semiring for that. Use take \max to be the sum and \times for the product et voilà, were’ done.
mostReliablePath :: (KnownNat n, Ord r, Num r)
=> [(Edge, r)] -> Matrix n (Tropical (Down (Product r), [Edge]))
= solveWithPath (Down . Product) mostReliablePath
Suppose now that you’re not worried about the message failing to be delivered, but with its size. You want to send a really big package and most know whether it can pass on the road. In order to guarantee the delivery, you decide to find the widest path on the graph. To find it, we define the width of a path as the minimum width among its edges (the bottleneck) and then aggregate among all paths by choosing the one with the largest bottleneck. That’s a semiring with \oplus = \max, and \otimes = \min.
widestPath :: (KnownNat n, Ord r, Bounded r)
=> [(Edge, r)] -> Matrix n (Tropical (Down (Min r), [Edge]))
= solveWithPath (Down . Min) widestPath
Besides those, a lot of other queries on graphs are realizable in terms of semirings, and specially some variation of the tropical semiring. A book with a lot of examples in the applied field of networks is the one by John S. Baras and George Theodorakopoulos5.
Transitive Closures of Relations
Let’s now divert our attention to another topic that is nevertheless closely related: finite relations. Think about the common binary relations that we work with everyday: equality, order (\le, <, \ge, >), equivalence relations… All of them share two important properties: transitivity and reflexivity.
So, if you’re going to work with some relation, a typical process is to complete it in order to turn this relation transitive and reflexive. Of course, your relation is just for the VIP and you don’t want to add every arbitrary pair of elements during this operation. You want to find the smallest reflexive-transitive relation containing yours.
This is called the reflexive-transitive closure of a relation. Now, how do we calculate this thing? As you may imagine, there is a semiring made just for that and the algorithm is exactly the same as for shortest paths. All we have to do is to use the Boolean semiring.
instance Semiring Bool where
= True
one = False
zero |+|) = (||)
(|*|) = (&&)
(= one -- Reflexivity for a single vertex closure _
Using the old trick of representing a finite relation as a Boolean matrix, the operation we need is exactly the closure for the matrix semiring.
reflexiveTransitive :: KnownNat n => Matrix n Bool -> Matrix n Bool
= closure reflexiveTransitive
Interestingly, the above can also be comprehended as a common operation on graphs. By translating the boolean matrix into the (unweighted) adjacency matrix of a directed graph, this closure returns the graph whose edges are all paths on the original graph. If one can go from s to t in G, then G^\star(s, t) = \mathtt{true}. In the figure below, we illustrate this relationship.
Free as in Regular Expressions
When we study some kind of algebraic structure, we tend to find out some kind of datastructure that is intimately related to it. For example, monoids have lists, magmas have binary trees, and vector spaces have fixed-length boxes of numbers. These datatypes are a way to represent the free version of the algebraic structure. The actual definition of something being free in mathematics is a bit too technical for this blog’s tone, but the important to us is that, in general, a free structure can act as a skeleton (or syntax tree) representing a computation and any other instance of the algebraic structure can be recovered with a suitable interpreter.6
Well, do you have any guess for what is the free closed semiring? Perhaps surprisingly, it consists of Regular Expressions, at least when we assume the sum to be idempotent.7 Yep, regular expressions, those strange grawlixes that perl programmers love so much and that renders your queries completely unreadable a couple of weeks after you’ve written them. Since I don’t want this page to be worthy of a comic book cursing context, we will follow a more disciplined approach and represent the regular expressions as its own a datatype.8
data Regex a = Nope -- Empty Set: matches nothing
| Empty -- Empty String: matches anything
| Literal a -- A single character from the alphabet
| Union (Regex a) (Regex a) -- matches either an argument or the other
| Join (Regex a) (Regex a) -- Matches one argument followed by the other
| Many (Regex a) -- Kleene star: Matches zero or more instances of something
Notice that we’re limiting ourselves to truly regular expressions, those that represent some kind of Regular Language.
The semiring instance is straightforward, since the constructors closely resemble the class methods. We will only implement a couple simplifications in order to not get expressions with redundant parts.
instance Eq a => Semiring (Regex a) where
= Nope
zero = Empty
one -- Union with some eliminations for empty elements
Nope |+| e = e
|+| Nope = e
e -- Remove empties when possible
Empty |+| (Many e) = Many e -- Star contains the empty string
Many e) |+| Empty = Many e
(Empty |+| Empty = Empty
|+| y = Union x y
x -- Concatenation and some simplifications
Nope |*| x = Nope -- Annihilation
|*| Nope = Nope
x Empty |*| x = x -- Identity
|*| Empty = x
x |*| y = Join x y
x -- Kleene star
Nope = Empty -- the closure is at least empty
closure Empty = Empty -- many instances of something empty
closure Many e) = Many e -- idempotence of Kleene star
closure (= Many e closure e
The previous definition was long but rather mechanical and, most important of all, gave us a shining new semiring to play with! Alright, what does the closure of a matrix of regular expressions mean? First, a graph labeled with regular expressions is exactly a finite state machine (more precisely, a ε-NFA), and since in this case \oplus is union and \otimes is concatenation, the power series interpretation implies that the closure A^* is the component-wise union of all fixed length paths one can follow in this automaton. Hence, each component A^*(s, t) is a regular expression representing the language accepted by this automaton with initial state s and accepting state t.
What is the Floyd-Warshall implementation, when specialized to this context? Here it becomes Kleene’s algorithm for converting a finite state machine into a regular expression.
As we discussed earlier, regular expressions are the free closed (idempotent) semiring. This means that any operation on semirings, such as finding shortest-paths, may be first calculated on regexes and then converted to the proper type. We achieve this with an interpreter function.
interpret :: Semiring r => (a -> r) -> (Regex a -> r)
Nope = zero
interpret f Empty = one
interpret f Literal a) = f a
interpret f (Union x y) = interpret f x |+| interpret f y
interpret f (Join x y) = interpret f x |*| interpret f y
interpret f (Many e) = closure (interpret f e) interpret f (
This is similar to how Haskell’s foldMap
works for monoids or the eval
function that
we defined
for calculus expressions on another post. All of
them are some kind of “realizations of typeclass”.
The usefulness of this interpreter comes from the fact that it commutes with the matrix operations. Thus, whenever we want to perform many queries in a same graph with idempotent aggregations (such as shortest paths, largest paths, most reliable paths, widest paths etc.), we may first calculate the regular expressions representing these paths and then collapse them separately for each semiring.
Classical Matrix Inversion
To wrap up this post, let’s take a look at a semiring that is not idempotent: the classical field structure on the real/complex numbers. Or, to be technicality accurate, one of these fields complete with an extra point to amount for the non-invertibility of zero.
data Classical a = Field a | Extra
deriving Eq
instance (Eq a, Fractional a) => Semiring (Classical a) where
= Field 0
zero = Field 1
one Extra |+| _ = Extra
|+| Extra = Extra
_ Field x |+| Field y = Field (x + y)
Field 0 |*| _ = Field 0
|*| Field 0 = Field 0
_ Extra |*| _ = Extra
|*| Extra = Extra
_ Field x |*| Field y = Field (x * y)
For a field, the closure operation has, in general, a closed form solution:
x^\star = 1 + x\cdot x^* \implies x^* = (1 - x)^{-1}.
Great, I must admit that I had already started missing being able to subtract and divide our numbers… Unfortunately, not everything is flowers and the number 1 is non-invertible. That’s why we had to add an extra point to the structure.
Extra = Extra
closure Field 1) = Extra
closure (Field x) = Field $ recip (1 - x) closure (
Since matrices have themselves a notion of inverse, their closure also satisfies the equation B^* = (I - B)^{-1} whenever the inverse on the right makes sense. With a change of variables I - B \to A, we arrive at a formula for matrix inversion:
A^{-1} = (I - A)^*.
Furthermore, this works whenever the right-hand side
has no Extra
terms, meaning that we’ve
arrived at a safe inversion formula for a matrix.
inv :: (Fractional a, Eq a, KnownNat n) => Matrix n a -> Maybe (Matrix n a)
= traverse toMaybe . closure . conj
inv where
= one |+| fmap (Field . negate) a -- A -> I - A
conj a Extra = Nothing
toMaybe Field a) = Just a toMaybe (
To which classical algorithm is this equivalent? By looking attentively at the definition of closure, you will see that this is exactly the Gauss-Jordan elimination, where in our case multiplying by the closure takes the same role as dividing by a row in the classical presentation.
Farewell
This was a lot of fun, but every post must come to an end. Most of it, I’ve learned from Baras and Theodorakopoulos9, which also contains a lot of examples in the context of networks. If you want to go deeper on tropical mathematics and the theory of closed semirings, the best book I know is Michel Gondran and Michel Minoux10. A lot of the implementations on this site where inspired by those from Stephen Dolan11’s paper and the very interesting blog post by Russel O’Connor12.
Farewell, and have fun spotting shortest paths where you would never imagine before. Heads up, there’s a lot of semirings lurking around.
Assuming you don’t consider Kleisli categories are as dirty as arithmetic with infinities.↩︎
As if this posts wasn’t abstract enough.↩︎
This is very similar to how a linked list can be viewed either as a pair containing a value and a reference to another list or as a vector that can have any possible length.↩︎
There is also a pretty elegant alternative that, unfortunately, only works for idempotent semirings. It is a little slower (O(n^3 \log n) versus O(n^3)) but it is worth showing here. If R is idempotent, then all cross-terms in a binomial cancel out and we get (I \oplus A)^n = \bigoplus_{k=0}^n A^k = A^\star. From this equation follows a pretty sleek one-liner:
closure a = (one |+| a) |^| nat @n
Sadly, it requires idempotence…↩︎
Path Problems in Networks, Synthesis Lectures on Learning, Networks, and Algorithms (Cham: Springer International Publishing, 2010), https://doi.org/10.1007/978-3-031-79983-9.↩︎
As always in mathematics there are some technicalities about when this holds. That’s a cool topic related to monads and CT, but too out of scope for this discussion. I suggest the books: Paolo Perrone, “Notes on Category Theory with Examples from Basic Mathematics,” February 2021, chap. 5, https://doi.org/10.48550/arXiv.1912.10642 and Emily Riehl, Category Theory in Context, n.d., chap. 5, https://math.jhu.edu/~eriehl/context/.↩︎
To be precise, regular expressions are the free Kleene Algebra.↩︎
For a primer on regular expressions in Haskell, see Pedro Vasconcelos’ A regular expression matcher.↩︎
Path Problems in Networks.↩︎
Graphs, Dioids and Semirings, vol. 41, Operations Research/Computer Science Interfaces (Boston, MA: Springer US, 2008), https://doi.org/10.1007/978-0-387-75450-5.↩︎
“Fun with Semirings: A Functional Pearl on the Abuse of Linear Algebra,” in Proceedings of the 18th ACM SIGPLAN International Conference on Functional Programming (Boston Massachusetts USA: ACM, 2013), 101–10, https://doi.org/10.1145/2500365.2500613.↩︎
“A Very General Method of Computing Shortest Paths,” n.d., https://r6.ca/blog/20110808T035622Z.html.↩︎