Current section
Files
Jump to
Current section
Files
src/graph.gleam
////
//// Here's a handy index you can use to browse through the various graph
//// functions.
////
//// | operation kind | functions |
//// |---|---|
//// | creating graphs | [`new`](#new) |
//// | turning graphs into lists | [`nodes`](#nodes) |
//// | querying a graph | [`size`](#size), [`has_node`](#has_node), [`has_edge`](#has_edge), [`get_context`](#get_context), [`match`](#match) |
//// | adding/removing elements from a graph | [`insert_node`](#insert_node), [`insert_directed_edge`](#insert_directed_edge), [`insert_undirected_edge`](#insert_undirected_edge), [`remove_node`](#remove_node), [`remove_directed_edge`](#remove_directed_edge), [`remove_undirected_edge`](#remove_undirected_edge) |
//// | transforming graphs | [`fold`](#fold), [`reverse`](#reverse), [`map_contexts`](#map_contexts), [`map_values`](#map_values), [`map_labels`](#map_labels), [`reverse_edges`](#reverse_edges), [`to_directed`](#to_directed) |
////
import gleam/dict.{type Dict}
import gleam/result
// --- THE GRAPH TYPE ----------------------------------------------------------
/// The direction of a directed graph.
///
pub type Directed
/// The direction of an undirected graph.
///
pub type Undirected
/// A directed or undirected graph. A graph is made up of nodes and edges
/// connecting them: each node holds a `value` and each edge has a `label`.
///
/// The graph also carries along its `direction` (either `Directed` or
/// `Undirected`) in its type so that it's impossible to mix up `Directed` and
/// `Undirected` graphs inadvertently.
///
pub opaque type Graph(direction, value, label) {
Graph(Dict(Int, Context(value, label)))
}
/// A node making up a graph. Every node is identified by a number and can hold
/// an arbitrary value.
///
pub type Node(value) {
Node(id: Int, value: value)
}
/// The context associated with a node in a graph: it contains the node itself
/// and all the incoming and outgoing edges. Edges are stored in a dict going
/// from neighbour's id to the edge label.
///
pub type Context(value, label) {
Context(
incoming: Dict(Int, label),
node: Node(value),
outgoing: Dict(Int, label),
)
}
// --- CREATING GRAPHS ---------------------------------------------------------
/// Creates a new empty graph.
///
/// ## Examples
///
/// ```gleam
/// nodes(new())
/// // -> []
/// ```
///
pub fn new() -> Graph(direction, value, label) {
Graph(dict.new())
}
// --- TURNING GRAPHS INTO LISTS -----------------------------------------------
/// Returns a list of all the nodes contained in the graph.
///
/// ## Examples
///
/// ```gleam
/// new() |> nodes
/// // -> []
/// ```
///
/// ```gleam
/// new() |> insert_node(Node(1, "a node")) |> nodes
/// // -> [Node(1, "a node")]
/// ```
///
pub fn nodes(graph: Graph(direction, value, label)) -> List(Node(value)) {
let Graph(graph) = graph
use acc, _node_id, Context(node: node, ..) <- dict.fold(over: graph, from: [])
[node, ..acc]
}
// --- QUERYING A GRAPH --------------------------------------------------------
/// Returns the number of nodes of the graph.
///
/// ## Examples
///
/// ```gleam
/// new() |> size
/// // -> 0
/// ```
///
/// ```gleam
/// new() |> insert_node(Node(1, "a node")) |> size
/// // -> 1
/// ```
///
pub fn size(graph: Graph(direction, value, label)) -> Int {
let Graph(graph) = graph
dict.size(graph)
}
/// Returns `True` if the graph contains a node with the given id.
///
/// ## Examples
///
/// ```gleam
/// let my_graph = new() |> insert_node(Node(1, "a node"))
///
/// my_graph |> has_node(1)
/// // -> True
///
/// my_graph |> has_node(2)
/// // -> False
/// ```
///
pub fn has_node(graph: Graph(direction, value, label), node_id: Int) -> Bool {
let Graph(graph) = graph
dict.has_key(graph, node_id)
}
/// Returns `True` if the graph has an edge connecting the two nodes with the
/// given ids.
///
/// ## Examples
///
/// ```gleam
/// let my_graph =
/// new()
/// |> insert_node(Node(1, "a node"))
/// |> insert_node(Node(2, "other node"))
/// |> insert_directed_edge("edge label", from: 1, to: 2)
///
/// my_graph |> has_edge(from: 1, to: 2)
/// // -> True
///
/// my_graph |> has_edge(from: 2, to: 1)
/// // -> False
/// ```
///
pub fn has_edge(
graph: Graph(direction, value, label),
from source: Int,
to destination: Int,
) -> Bool {
case get_context(graph, source) {
Ok(Context(outgoing: outgoing, ..)) -> dict.has_key(outgoing, destination)
Error(_) -> False
}
}
/// Returns the context associated with the node with the given id, if present.
/// Otherwise returns `Error(Nil)`.
///
/// ## Examples
///
/// ```gleam
/// new() |> get(1)
/// // -> Error(Nil)
/// ```
///
/// ```gleam
/// new() |> insert_node(Node(1, "a node")) |> get_context(of: 1)
/// // -> Ok(Context(node: Node(1, "a node"), ..))
/// ```
///
pub fn get_context(
graph: Graph(direction, value, label),
of node: Int,
) -> Result(Context(value, label), Nil) {
let Graph(graph) = graph
dict.get(graph, node)
}
/// If the graph contains a node with the given id, returns a tuple containing
/// the context of that node (with all edges looping back to itself removed) and
/// the "remaining" graph: that is, the original graph where that node has been
/// removed.
///
pub fn match(
graph: Graph(direction, value, label),
node_id: Int,
) -> Result(#(Context(value, label), Graph(direction, value, label)), Nil) {
use Context(incoming, node, outgoing) <- result.try(get_context(
graph,
node_id,
))
let rest = remove_node(graph, node_id)
let new_incoming = dict.delete(incoming, node_id)
let new_outgoing = dict.delete(outgoing, node_id)
Ok(#(Context(new_incoming, node, new_outgoing), rest))
}
// --- ADDING/REMOVING ELEMENTS FROM A GRAPH -----------------------------------
/// Adds a node to the given graph.
/// If the graph already contains a node with the same id, that will be replaced
/// by the new one.
/// The newly added node won't be connected to any existing node.
///
/// ## Examples
///
/// ```gleam
/// new() |> insert_node(Node(1, "a node")) |> nodes
/// // -> [Node(1, "a node")]
/// ```
///
pub fn insert_node(
graph: Graph(direction, value, label),
node: Node(value),
) -> Graph(direction, value, label) {
let Graph(graph) = graph
let empty_context = Context(dict.new(), node, dict.new())
let new_graph = dict.insert(graph, node.id, empty_context)
Graph(new_graph)
}
/// Adds an edge connecting two nodes in a directed graph.
///
/// ```gleam
/// let my_graph =
/// new()
/// |> insert_node(Node(1, "a node"))
/// |> insert_node(Node(2, "other node"))
/// |> insert_directed_edge("edge label", from: 1, to: 2)
///
/// my_graph |> has_edge(from: 1, to: 2)
/// // -> True
///
/// my_graph |> has_edge(from: 2, to: 1)
/// // -> False
/// ```
///
pub fn insert_directed_edge(
graph: Graph(Directed, value, label),
labelled label: label,
from source: Int,
to destination: Int,
) -> Graph(Directed, value, label) {
graph
|> update_context(of: source, with: add_outgoing_edge(_, destination, label))
|> update_context(of: destination, with: add_incoming_edge(_, source, label))
}
/// Adds an edge connecting two nodes in an undirected graph.
///
/// ## Examples
///
/// ```gleam
/// let my_graph =
/// new()
/// |> insert_node(Node(1, "a node"))
/// |> insert_node(Node(2, "other node"))
/// |> insert_undirected_edge("edge label", between: 1, and: 2)
///
/// my_graph |> has_edge(from: 1, to: 2)
/// // -> True
///
/// my_graph |> has_edge(from: 2, to: 1)
/// // -> True
/// ```
pub fn insert_undirected_edge(
graph: Graph(Undirected, value, label),
labelled label: label,
between one: Int,
and other: Int,
) -> Graph(Undirected, value, label) {
graph
|> update_context(of: one, with: fn(context) {
add_outgoing_edge(context, other, label)
|> add_incoming_edge(other, label)
})
|> update_context(of: other, with: fn(context) {
add_outgoing_edge(context, one, label)
|> add_incoming_edge(one, label)
})
}
fn update_context(
in graph: Graph(direction, value, label),
of node: Int,
with fun: fn(Context(value, label)) -> Context(value, label),
) -> Graph(direction, value, label) {
let Graph(graph) = graph
case dict.get(graph, node) {
Ok(context) -> Graph(dict.insert(graph, node, fun(context)))
Error(_) -> Graph(graph)
}
}
fn add_outgoing_edge(
context: Context(value, label),
to node: Int,
labelled label: label,
) -> Context(value, label) {
let Context(outgoing: outgoing, ..) = context
Context(..context, outgoing: dict.insert(outgoing, node, label))
}
fn remove_outgoing_edge(
context: Context(value, label),
to node: Int,
) -> Context(value, label) {
let Context(outgoing: outgoing, ..) = context
Context(..context, outgoing: dict.delete(outgoing, node))
}
fn add_incoming_edge(
context: Context(value, label),
from node: Int,
labelled label: label,
) -> Context(value, label) {
let Context(incoming: incoming, ..) = context
Context(..context, incoming: dict.insert(incoming, node, label))
}
fn remove_incoming_edge(
context: Context(value, label),
from node: Int,
) -> Context(value, label) {
let Context(incoming: incoming, ..) = context
Context(..context, incoming: dict.delete(incoming, node))
}
/// Removes a node with the given id from the graph. If there's no node with the
/// given id it does nothing.
///
pub fn remove_node(
graph: Graph(direction, value, label),
node_id: Int,
) -> Graph(direction, value, label) {
case graph, get_context(graph, node_id) {
_, Error(_) -> graph
Graph(graph), Ok(Context(incoming, _, outgoing)) ->
dict.delete(graph, node_id)
|> remove_incoming_occurrences(of: node_id, from: outgoing)
|> remove_outgoing_occurrences(of: node_id, from: incoming)
|> Graph
}
}
fn remove_incoming_occurrences(
in graph: Dict(Int, Context(value, label)),
of node: Int,
from nodes: Dict(Int, a),
) -> Dict(Int, Context(value, label)) {
use context, _ <- dict_map_shared_keys(graph, with: nodes)
let Context(incoming: incoming, ..) = context
Context(..context, incoming: dict.delete(incoming, node))
}
fn remove_outgoing_occurrences(
in graph: Dict(Int, Context(value, label)),
of node: Int,
from nodes: Dict(Int, a),
) -> Dict(Int, Context(value, label)) {
use context, _ <- dict_map_shared_keys(graph, with: nodes)
let Context(outgoing: outgoing, ..) = context
Context(..context, outgoing: dict.delete(outgoing, node))
}
/// Removes a directed edge connecting two nodes from a graph.
///
pub fn remove_directed_edge(
graph: Graph(Directed, value, label),
from source: Int,
to destination: Int,
) -> Graph(Directed, value, label) {
graph
|> update_context(of: source, with: remove_outgoing_edge(_, to: destination))
|> update_context(of: destination, with: remove_incoming_edge(_, from: source))
}
/// Removes an undirected edge connecting two nodes from a graph.
///
pub fn remove_undirected_edge(
graph: Graph(Undirected, value, label),
between one: Int,
and other: Int,
) -> Graph(Undirected, value, label) {
graph
|> update_context(of: one, with: fn(context) {
remove_outgoing_edge(context, to: other)
|> remove_incoming_edge(from: other)
})
|> update_context(of: other, with: fn(context) {
remove_outgoing_edge(context, to: one)
|> remove_incoming_edge(from: one)
})
}
// --- TRANSFORMING GRAPHS -----------------------------------------------------
/// Reduces the given graph into a single value by applying function to all its
/// contexts, one after the other.
///
/// > 🚨 Graph's contexts are not sorted in any way so your folding function
/// > should never rely on any accidental order the contexts might have.
///
/// ## Examples
///
/// ```gleam
/// // The size function could be implemented using a fold.
/// // The real implementation is more efficient because it doesn't have to
/// // traverse all contexts!
/// pub fn size(graph) {
/// fold(
/// over: graph,
/// from: 0,
/// with: fn(size, _context) { size + 1 },
/// )
/// }
/// ```
///
pub fn fold(
over graph: Graph(direction, value, label),
from initial: b,
with fun: fn(b, Context(value, label)) -> b,
) -> b {
let Graph(graph) = graph
use acc, _node_id, context <- dict.fold(over: graph, from: initial)
fun(acc, context)
}
/// Transform the contexts associated with each node.
///
/// > This function can add and remove arbitrary edges from the graph by
/// > updating the `incoming` and `outgoing` edges of a context.
/// > So we can't assume the final graph will still be `Undirected`, that's why
/// > it is always treated as a `Directed` one.
///
/// ## Examples
///
/// ```gleam
/// // The reverse function can be implemented with `map_contexts`
/// pub fn reverse(graph) {
/// map_contexts(in: graph, with: fn(context) {
/// Context(
/// ..context,
/// incoming: context.outgoing,
/// outgoing: context.incoming,
/// )
/// })
/// }
/// ```
///
pub fn map_contexts(
in graph: Graph(direction, value, label),
with fun: fn(Context(value, label)) -> Context(value, label),
) -> Graph(Directed, value, label) {
use acc, context <- fold(over: graph, from: new())
insert_context(acc, fun(context))
}
fn insert_context(
graph: Graph(direction, value, label),
context: Context(value, label),
) -> Graph(Directed, value, label) {
let Graph(graph) = graph
let new_graph = dict.insert(graph, context.node.id, context)
Graph(new_graph)
}
/// Transforms the values of all the graph's nodes using the given function.
///
/// ## Examples
///
/// ```gleam
/// new()
/// |> insert_node(Node(1, "a node"))
/// |> map_nodes(fn(value) { value <> "!" })
/// |> nodes
/// // -> [Node(1, "my node!")]
/// ```
///
pub fn map_values(
in graph: Graph(direction, value, label),
with fun: fn(value) -> new_value,
) -> Graph(direction, new_value, label) {
let Graph(graph) = graph
// Since this function doesn't change the graph's topology I'm not
// implementing it with a `graph.fold` or a `graph.map_contexts`, it would
// increase code reuse but would rebuild a new graph each time by adding
// each context one by one.
Graph({
use _node_id, context <- dict.map_values(graph)
let Context(incoming, Node(id, value), outgoing) = context
Context(incoming, Node(id, fun(value)), outgoing)
})
}
/// Transforms the labels of all the graph's edges using the given function.
///
/// ## Examples
///
/// ```
/// new()
/// |> insert_node(Node(1, "a node"))
/// |> insert_undirected_edge(UndirectedEdge(1, 1, "label"))
/// |> map_labels(fn(label) { label <> "!" })
/// |> labels
/// // -> ["label!"]
/// ```
///
pub fn map_labels(
in graph: Graph(direction, value, label),
with fun: fn(label) -> new_label,
) -> Graph(direction, value, new_label) {
// Since this function doesn't change the graph's topology I'm not
// implementing it with a `graph.fold` or a `graph.map_contexts`, it would
// increase code reuse but would rebuild a new graph each time by adding
// each context one by one.
let Graph(graph) = graph
Graph({
use _node_id, context <- dict.map_values(graph)
let Context(incoming, node, outgoing) = context
let new_incoming = dict.map_values(incoming, fn(_id, label) { fun(label) })
let new_outgoing = dict.map_values(outgoing, fn(_id, label) { fun(label) })
Context(new_incoming, node, new_outgoing)
})
}
/// Flips the direction of every edge in the graph. All incoming edges will
/// become outgoing and vice-versa.
///
pub fn reverse_edges(
graph: Graph(Directed, value, label),
) -> Graph(Directed, value, label) {
// Since this function doesn't change the graph's structure I'm not
// implementing it with a `graph.fold` or a `graph.map_contexts`, it would
// increase code reuse but would rebuild a new graph each time by adding
// each context one by one
let Graph(graph) = graph
Graph({
use _node_id, context <- dict.map_values(graph)
let Context(incoming, node, outgoing) = context
Context(outgoing, node, incoming)
})
}
/// Turns an undirected graph into a directed one. Every edge connecting two
/// nodes in the original graph will be considered as a pair of edges connecting
/// the nodes going in both directions.
///
pub fn to_directed(
graph: Graph(Undirected, value, label),
) -> Graph(Directed, value, label) {
let Graph(graph) = graph
Graph(graph)
}
// --- DICT UTILITY FUNCTIONS --------------------------------------------------
fn dict_map_shared_keys(
in one: Dict(k, a),
with other: Dict(k, b),
using fun: fn(a, b) -> a,
) -> Dict(k, a) {
use one, key, other_value <- dict.fold(over: other, from: one)
case dict.get(one, key) {
Ok(one_value) -> dict.insert(one, key, fun(one_value, other_value))
Error(_) -> one
}
}