Current section
Files
Jump to
Current section
Files
src/yog@transform.erl
-module(yog@transform).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/yog/transform.gleam").
-export([transpose/1, map_nodes/2, map_edges/2, filter_nodes/2, merge/2]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-file("src/yog/transform.gleam", 32).
?DOC(
" Reverses the direction of every edge in the graph (graph transpose).\n"
"\n"
" Due to the dual-map representation (storing both out_edges and in_edges),\n"
" this is an **O(1) operation** - just a pointer swap! This is dramatically\n"
" faster than most graph libraries where transpose is O(E).\n"
"\n"
" **Time Complexity:** O(1)\n"
"\n"
" **Property:** `transpose(transpose(G)) = G`\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let graph =\n"
" model.new(Directed)\n"
" |> model.add_edge(from: 1, to: 2, with: 10)\n"
" |> model.add_edge(from: 2, to: 3, with: 20)\n"
"\n"
" let reversed = transform.transpose(graph)\n"
" // Now has edges: 2->1 and 3->2\n"
" ```\n"
"\n"
" ## Use Cases\n"
"\n"
" - Computing strongly connected components (Kosaraju's algorithm)\n"
" - Finding all nodes that can reach a target node\n"
" - Reversing dependencies in a DAG\n"
).
-spec transpose(yog@model:graph(GIX, GIY)) -> yog@model:graph(GIX, GIY).
transpose(Graph) ->
{graph,
erlang:element(2, Graph),
erlang:element(3, Graph),
erlang:element(5, Graph),
erlang:element(4, Graph)}.
-file("src/yog/transform.gleam", 70).
?DOC(
" Transforms node data using a function, preserving graph structure.\n"
"\n"
" This is a functor operation - it applies a function to every node's data\n"
" while keeping all edges and the graph structure unchanged.\n"
"\n"
" **Time Complexity:** O(V) where V is the number of nodes\n"
"\n"
" **Functor Law:** `map_nodes(map_nodes(g, f), h) = map_nodes(g, fn(x) { h(f(x)) })`\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let graph =\n"
" model.new(Directed)\n"
" |> model.add_node(1, \"alice\")\n"
" |> model.add_node(2, \"bob\")\n"
"\n"
" let uppercased = transform.map_nodes(graph, string.uppercase)\n"
" // Nodes now contain \"ALICE\" and \"BOB\"\n"
" ```\n"
"\n"
" ## Type Changes\n"
"\n"
" Can change the node data type:\n"
"\n"
" ```gleam\n"
" // Convert string node data to integers\n"
" transform.map_nodes(graph, fn(s) {\n"
" case int.parse(s) {\n"
" Ok(n) -> n\n"
" Error(_) -> 0\n"
" }\n"
" })\n"
" ```\n"
).
-spec map_nodes(yog@model:graph(GJD, GJE), fun((GJD) -> GJH)) -> yog@model:graph(GJH, GJE).
map_nodes(Graph, Fun) ->
New_nodes = begin
_pipe = maps:to_list(erlang:element(3, Graph)),
_pipe@1 = gleam@list:map(
_pipe,
fun(Pair) ->
{erlang:element(1, Pair), Fun(erlang:element(2, Pair))}
end
),
maps:from_list(_pipe@1)
end,
{graph,
erlang:element(2, Graph),
New_nodes,
erlang:element(4, Graph),
erlang:element(5, Graph)}.
-file("src/yog/transform.gleam", 117).
?DOC(
" Transforms edge weights using a function, preserving graph structure.\n"
"\n"
" This is a functor operation - it applies a function to every edge's weight/data\n"
" while keeping all nodes and the graph topology unchanged.\n"
"\n"
" **Time Complexity:** O(E) where E is the number of edges\n"
"\n"
" **Functor Law:** `map_edges(map_edges(g, f), h) = map_edges(g, fn(x) { h(f(x)) })`\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let graph =\n"
" model.new(Directed)\n"
" |> model.add_edge(from: 1, to: 2, with: 10)\n"
" |> model.add_edge(from: 2, to: 3, with: 20)\n"
"\n"
" // Double all weights\n"
" let doubled = transform.map_edges(graph, fn(w) { w * 2 })\n"
" // Edges now have weights 20 and 40\n"
" ```\n"
"\n"
" ## Type Changes\n"
"\n"
" Can change the edge weight type:\n"
"\n"
" ```gleam\n"
" // Convert integer weights to floats\n"
" transform.map_edges(graph, int.to_float)\n"
"\n"
" // Convert weights to labels\n"
" transform.map_edges(graph, fn(w) {\n"
" case w < 10 {\n"
" True -> \"short\"\n"
" False -> \"long\"\n"
" }\n"
" })\n"
" ```\n"
).
-spec map_edges(yog@model:graph(GJK, GJL), fun((GJL) -> GJO)) -> yog@model:graph(GJK, GJO).
map_edges(Graph, Fun) ->
Transform_inner = fun(Inner_map) -> _pipe = maps:to_list(Inner_map),
_pipe@1 = gleam@list:map(
_pipe,
fun(Pair) ->
{erlang:element(1, Pair), Fun(erlang:element(2, Pair))}
end
),
maps:from_list(_pipe@1) end,
Transform_outer = fun(Outer_map) -> _pipe@2 = maps:to_list(Outer_map),
_pipe@3 = gleam@list:map(
_pipe@2,
fun(Pair@1) ->
{erlang:element(1, Pair@1),
Transform_inner(erlang:element(2, Pair@1))}
end
),
maps:from_list(_pipe@3) end,
{graph,
erlang:element(2, Graph),
erlang:element(3, Graph),
Transform_outer(erlang:element(4, Graph)),
Transform_outer(erlang:element(5, Graph))}.
-file("src/yog/transform.gleam", 168).
?DOC(
" Filters nodes by a predicate, automatically pruning connected edges.\n"
"\n"
" Returns a new graph containing only nodes whose data satisfies the predicate.\n"
" All edges connected to removed nodes (both incoming and outgoing) are\n"
" automatically removed to maintain graph consistency.\n"
"\n"
" **Time Complexity:** O(V + E) where V is nodes and E is edges\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let graph =\n"
" model.new(Directed)\n"
" |> model.add_node(1, \"apple\")\n"
" |> model.add_node(2, \"banana\")\n"
" |> model.add_node(3, \"apricot\")\n"
" |> model.add_edge(from: 1, to: 2, with: 1)\n"
" |> model.add_edge(from: 2, to: 3, with: 2)\n"
"\n"
" // Keep only nodes starting with 'a'\n"
" let filtered = transform.filter_nodes(graph, fn(s) {\n"
" string.starts_with(s, \"a\")\n"
" })\n"
" // Result has nodes 1 and 3, edge 1->2 is removed (node 2 gone)\n"
" ```\n"
"\n"
" ## Use Cases\n"
"\n"
" - Extract subgraphs based on node properties\n"
" - Remove inactive/disabled nodes from a network\n"
" - Filter by node importance/centrality\n"
).
-spec filter_nodes(yog@model:graph(GJR, GJS), fun((GJR) -> boolean())) -> yog@model:graph(GJR, GJS).
filter_nodes(Graph, Predicate) ->
Kept_nodes = gleam@dict:filter(
erlang:element(3, Graph),
fun(_, Data) -> Predicate(Data) end
),
Kept_ids = maps:keys(Kept_nodes),
Prune_edges = fun(Outer_map) ->
_pipe = gleam@dict:filter(
Outer_map,
fun(Src, _) -> gleam@list:contains(Kept_ids, Src) end
),
gleam@dict:map_values(
_pipe,
fun(_, Inner_map) ->
gleam@dict:filter(
Inner_map,
fun(Dst, _) -> gleam@list:contains(Kept_ids, Dst) end
)
end
)
end,
{graph,
erlang:element(2, Graph),
Kept_nodes,
Prune_edges(erlang:element(4, Graph)),
Prune_edges(erlang:element(5, Graph))}.
-file("src/yog/transform.gleam", 224).
?DOC(
" Combines two graphs, with the second graph's data taking precedence on conflicts.\n"
"\n"
" Merges nodes, out_edges, and in_edges from both graphs. When a node or edge\n"
" exists in both graphs, the data from the `other` graph overwrites the `base`.\n"
"\n"
" The resulting graph uses the `kind` (Directed/Undirected) from the base graph.\n"
"\n"
" **Time Complexity:** O(V + E) for both graphs combined\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let base =\n"
" model.new(Directed)\n"
" |> model.add_node(1, \"Original\")\n"
" |> model.add_edge(from: 1, to: 2, with: 10)\n"
"\n"
" let other =\n"
" model.new(Directed)\n"
" |> model.add_node(1, \"Updated\")\n"
" |> model.add_edge(from: 2, to: 3, with: 20)\n"
"\n"
" let merged = transform.merge(base, other)\n"
" // Node 1 has \"Updated\" (from other)\n"
" // Has edges: 1->2 (weight 10) and 2->3 (weight 20)\n"
" ```\n"
"\n"
" ## Use Cases\n"
"\n"
" - Combining disjoint subgraphs\n"
" - Applying updates/patches to a graph\n"
" - Building graphs incrementally from multiple sources\n"
).
-spec merge(yog@model:graph(GJX, GJY), yog@model:graph(GJX, GJY)) -> yog@model:graph(GJX, GJY).
merge(Base, Other) ->
Merge_outer = fun(M1, M2) -> maps:merge(M1, M2) end,
{graph,
erlang:element(2, Base),
maps:merge(erlang:element(3, Base), erlang:element(3, Other)),
Merge_outer(erlang:element(4, Base), erlang:element(4, Other)),
Merge_outer(erlang:element(5, Base), erlang:element(5, Other))}.