Current section

Files

Jump to
yog src yog@model.erl
Raw

src/yog@model.erl

-module(yog@model).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/yog/model.gleam").
-export([new/1, add_node/3, successors/2, predecessors/2, neighbors/2, all_nodes/1, order/1, node_count/1, edge_count/1, successor_ids/2, add_edge/4, add_edge_ensure/5, add_edge_with/5, add_edges/2, add_simple_edges/2, add_unweighted_edges/2, remove_node/2, remove_edge/3, add_edge_with_combine/5]).
-export_type([graph_type/0, graph/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.
?MODULEDOC(
" Core graph data structures and basic operations for the yog library.\n"
"\n"
" This module defines the fundamental `Graph` type and provides all basic operations\n"
" for creating and manipulating graphs. The graph uses an adjacency list representation\n"
" with dual indexing (both outgoing and incoming edges) for efficient traversal in both\n"
" directions.\n"
"\n"
" ## Graph Types\n"
"\n"
" - **Directed Graph**: Edges have a direction (one-way relationships)\n"
" - **Undirected Graph**: Edges are bidirectional (mutual relationships)\n"
"\n"
" ## Type Parameters\n"
"\n"
" - `node_data`: The type of data stored at each node (e.g., `String`, `City`, `Task`)\n"
" - `edge_data`: The type of data stored on edges, typically weights (e.g., `Int`, `Float`)\n"
"\n"
" ## Quick Start\n"
"\n"
" ```gleam\n"
" import yog/model\n"
"\n"
" let assert Ok(graph) =\n"
" model.new(model.Undirected)\n"
" |> model.add_node(1, \"Alice\")\n"
" |> model.add_node(2, \"Bob\")\n"
" |> model.add_edge(from: 1, to: 2, with: 10) // weight = 10\n"
" ```\n"
"\n"
" ## Design Notes\n"
"\n"
" The dual-map representation enables O(1) edge existence checks and O(1) transpose\n"
" operations, at the cost of increased memory usage and slightly more complex edge\n"
" updates.\n"
).
-type graph_type() :: directed | undirected.
-type graph(DMD, DME) :: {graph,
graph_type(),
gleam@dict:dict(integer(), DMD),
gleam@dict:dict(integer(), gleam@dict:dict(integer(), DME)),
gleam@dict:dict(integer(), gleam@dict:dict(integer(), DME))}.
-file("src/yog/model.gleam", 77).
?DOC(
" Creates a new empty graph of the specified type.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let graph = model.new(Directed)\n"
" ```\n"
).
-spec new(graph_type()) -> graph(any(), any()).
new(Graph_type) ->
{graph, Graph_type, maps:new(), maps:new(), maps:new()}.
-file("src/yog/model.gleam", 96).
?DOC(
" Adds a node to the graph with the given ID and data.\n"
" If a node with this ID already exists, its data will be replaced.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" graph\n"
" |> model.add_node(1, \"Node A\")\n"
" |> model.add_node(2, \"Node B\")\n"
" ```\n"
).
-spec add_node(graph(DMJ, DMK), integer(), DMJ) -> graph(DMJ, DMK).
add_node(Graph, Id, Data) ->
New_nodes = gleam@dict:insert(erlang:element(3, Graph), Id, Data),
{graph,
erlang:element(2, Graph),
New_nodes,
erlang:element(4, Graph),
erlang:element(5, Graph)}.
-file("src/yog/model.gleam", 340).
?DOC(" Adds a node only if it doesn't already exist.\n").
-spec ensure_node(graph(DOO, DOP), integer(), DOO) -> graph(DOO, DOP).
ensure_node(Graph, Id, Data) ->
case gleam@dict:has_key(erlang:element(3, Graph), Id) of
true ->
Graph;
false ->
add_node(Graph, Id, Data)
end.
-file("src/yog/model.gleam", 349).
?DOC(
" Adds a node only if it doesn't already exist, using a function\n"
" to create the node data from the node ID.\n"
).
-spec ensure_node_with(graph(DOU, DOV), integer(), fun((integer()) -> DOU)) -> graph(DOU, DOV).
ensure_node_with(Graph, Id, Make) ->
case gleam@dict:has_key(erlang:element(3, Graph), Id) of
true ->
Graph;
false ->
add_node(Graph, Id, Make(Id))
end.
-file("src/yog/model.gleam", 361).
?DOC(" Gets nodes you can travel TO (Successors).\n").
-spec successors(graph(any(), DPB), integer()) -> list({integer(), DPB}).
successors(Graph, Id) ->
_pipe = erlang:element(4, Graph),
_pipe@1 = gleam_stdlib:map_get(_pipe, Id),
_pipe@2 = gleam@result:map(_pipe@1, fun maps:to_list/1),
gleam@result:unwrap(_pipe@2, []).
-file("src/yog/model.gleam", 369).
?DOC(" Gets nodes you came FROM (Predecessors).\n").
-spec predecessors(graph(any(), DPG), integer()) -> list({integer(), DPG}).
predecessors(Graph, Id) ->
_pipe = erlang:element(5, Graph),
_pipe@1 = gleam_stdlib:map_get(_pipe, Id),
_pipe@2 = gleam@result:map(_pipe@1, fun maps:to_list/1),
gleam@result:unwrap(_pipe@2, []).
-file("src/yog/model.gleam", 378).
?DOC(
" Gets everyone connected to the node, regardless of direction.\n"
" Useful for algorithms like finding \"connected connectivity.\"\n"
).
-spec neighbors(graph(any(), DPL), integer()) -> list({integer(), DPL}).
neighbors(Graph, Id) ->
case erlang:element(2, Graph) of
undirected ->
successors(Graph, Id);
directed ->
Outgoing = successors(Graph, Id),
Incoming = predecessors(Graph, Id),
Out_ids = gleam@set:from_list(
gleam@list:map(Outgoing, fun gleam@pair:first/1)
),
gleam@list:fold(
Incoming,
Outgoing,
fun(Acc, _use1) ->
{In_id, _} = Incoming@1 = _use1,
case gleam@set:contains(Out_ids, In_id) of
true ->
Acc;
false ->
[Incoming@1 | Acc]
end
end
)
end.
-file("src/yog/model.gleam", 397).
?DOC(
" Returns all node IDs in the graph.\n"
" This includes all nodes, even isolated nodes with no edges.\n"
).
-spec all_nodes(graph(any(), any())) -> list(integer()).
all_nodes(Graph) ->
maps:keys(erlang:element(3, Graph)).
-file("src/yog/model.gleam", 404).
?DOC(
" Returns the number of nodes in the graph (graph order).\n"
"\n"
" **Time Complexity:** O(1)\n"
).
-spec order(graph(any(), any())) -> integer().
order(Graph) ->
maps:size(erlang:element(3, Graph)).
-file("src/yog/model.gleam", 412).
?DOC(
" Returns the number of nodes in the graph.\n"
" Equivalent to `order(graph)`.\n"
"\n"
" **Time Complexity:** O(1)\n"
).
-spec node_count(graph(any(), any())) -> integer().
node_count(Graph) ->
order(Graph).
-file("src/yog/model.gleam", 422).
?DOC(
" Returns the number of edges in the graph.\n"
"\n"
" For undirected graphs, each edge is counted once (the pair {u, v}).\n"
" For directed graphs, each directed edge (u -> v) is counted once.\n"
"\n"
" **Time Complexity:** O(V)\n"
).
-spec edge_count(graph(any(), any())) -> integer().
edge_count(Graph) ->
_pipe = gleam@dict:fold(
erlang:element(4, Graph),
0,
fun(Acc, _, Targets) -> Acc + maps:size(Targets) end
),
(fun(Count) -> case erlang:element(2, Graph) of
directed ->
Count;
undirected ->
Count div 2
end end)(_pipe).
-file("src/yog/model.gleam", 436).
?DOC(
" Returns just the NodeIds of successors (without edge weights).\n"
" Convenient for traversal algorithms that only need the IDs.\n"
).
-spec successor_ids(graph(any(), any()), integer()) -> list(integer()).
successor_ids(Graph, Id) ->
_pipe = successors(Graph, Id),
gleam@list:map(_pipe, fun(Edge) -> erlang:element(1, Edge) end).
-file("src/yog/model.gleam", 441).
-spec do_add_directed_edge(graph(DQL, DQM), integer(), integer(), DQM) -> graph(DQL, DQM).
do_add_directed_edge(Graph, Src, Dst, Weight) ->
Out_update_fn = fun(Maybe_inner_map) -> case Maybe_inner_map of
{some, M} ->
gleam@dict:insert(M, Dst, Weight);
none ->
maps:from_list([{Dst, Weight}])
end end,
In_update_fn = fun(Maybe_inner_map@1) -> case Maybe_inner_map@1 of
{some, M@1} ->
gleam@dict:insert(M@1, Src, Weight);
none ->
maps:from_list([{Src, Weight}])
end end,
New_out = gleam@dict:upsert(erlang:element(4, Graph), Src, Out_update_fn),
New_in = gleam@dict:upsert(erlang:element(5, Graph), Dst, In_update_fn),
{graph, erlang:element(2, Graph), erlang:element(3, Graph), New_out, New_in}.
-file("src/yog/model.gleam", 156).
?DOC(
" Adds an edge without checking if nodes exist (internal use).\n"
"\n"
" For directed graphs, adds a single edge from `src` to `dst`.\n"
" For undirected graphs, adds edges in both directions.\n"
"\n"
" > **Warning:** If `src` or `dst` have not been added via `add_node`,\n"
" > this creates \"ghost nodes\" that are traversable but invisible to\n"
" > functions that iterate over nodes (e.g. `order`, `filter_nodes`).\n"
" > Prefer using `add_edge` (checked) or `add_edge_ensure` (auto-creates).\n"
).
-spec add_edge_unchecked(graph(DMX, DMY), integer(), integer(), DMY) -> graph(DMX, DMY).
add_edge_unchecked(Graph, Src, Dst, Weight) ->
Graph@1 = do_add_directed_edge(Graph, Src, Dst, Weight),
case erlang:element(2, Graph@1) of
directed ->
Graph@1;
undirected ->
do_add_directed_edge(Graph@1, Dst, Src, Weight)
end.
-file("src/yog/model.gleam", 125).
?DOC(
" Adds an edge to the graph with the given weight.\n"
"\n"
" For directed graphs, adds a single edge from `src` to `dst`.\n"
" For undirected graphs, adds edges in both directions.\n"
"\n"
" Returns `Error` if either endpoint node doesn't exist in `graph.nodes`.\n"
" Use `add_edge_ensure` to auto-create missing nodes with a default value,\n"
" or `add_node` to explicitly add nodes before adding edges.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" graph\n"
" |> model.add_node(1, \"A\")\n"
" |> model.add_node(2, \"B\")\n"
" |> model.add_edge(from: 1, to: 2, with: 10)\n"
" // => Ok(graph)\n"
" ```\n"
"\n"
" ```gleam\n"
" graph\n"
" |> model.add_edge(from: 1, to: 2, with: 10)\n"
" // => Error(\"Node 1 does not exist\")\n"
" ```\n"
).
-spec add_edge(graph(DMP, DMQ), integer(), integer(), DMQ) -> {ok,
graph(DMP, DMQ)} |
{error, binary()}.
add_edge(Graph, Src, Dst, Weight) ->
case {gleam@dict:has_key(erlang:element(3, Graph), Src),
gleam@dict:has_key(erlang:element(3, Graph), Dst)} of
{true, true} ->
{ok, add_edge_unchecked(Graph, Src, Dst, Weight)};
{false, false} ->
{error,
<<<<<<<<"Nodes "/utf8, (erlang:integer_to_binary(Src))/binary>>/binary,
" and "/utf8>>/binary,
(erlang:integer_to_binary(Dst))/binary>>/binary,
" do not exist"/utf8>>};
{false, _} ->
{error,
<<<<"Node "/utf8, (erlang:integer_to_binary(Src))/binary>>/binary,
" does not exist"/utf8>>};
{_, false} ->
{error,
<<<<"Node "/utf8, (erlang:integer_to_binary(Dst))/binary>>/binary,
" does not exist"/utf8>>}
end.
-file("src/yog/model.gleam", 202).
?DOC(
" Ensures both endpoint nodes exist, then adds an edge.\n"
"\n"
" If `src` or `dst` is not already in the graph, it is created with\n"
" the supplied `default` node data before the edge is added. Nodes\n"
" that already exist are left unchanged.\n"
"\n"
" Always succeeds and returns a `Graph` (never fails).\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" // Nodes 1 and 2 are created automatically with data \"unknown\"\n"
" model.new(model.Directed)\n"
" |> model.add_edge_ensure(from: 1, to: 2, with: 10, default: \"unknown\")\n"
" ```\n"
"\n"
" ```gleam\n"
" // Existing nodes keep their data; only missing ones get the default\n"
" model.new(model.Directed)\n"
" |> model.add_node(1, \"Alice\")\n"
" |> model.add_edge_ensure(from: 1, to: 2, with: 5, default: \"anon\")\n"
" // Node 1 is still \"Alice\", node 2 is \"anon\"\n"
" ```\n"
" ## Future Improvements\n"
"\n"
" A future version may support separate defaults for each endpoint\n"
" (`default_from` and `default_to`). If you need this feature, please\n"
" [open an issue](https://github.com/code-shoily/yog/issues).\n"
"\n"
" For now, if you need different node values, then call `add_node` before\n"
" adding edges. Or call `add_edge_with()` with a callback function\n"
" that fetches the value of the NodeId (i.e. from a dict, database, or `identity`)\n"
).
-spec add_edge_ensure(graph(DND, DNE), integer(), integer(), DNE, DND) -> graph(DND, DNE).
add_edge_ensure(Graph, Src, Dst, Weight, Default) ->
Graph@1 = ensure_node(Graph, Src, Default),
Graph@2 = ensure_node(Graph@1, Dst, Default),
add_edge_unchecked(Graph@2, Src, Dst, Weight).
-file("src/yog/model.gleam", 237).
?DOC(
" Ensures both endpoint nodes exist using a callback, then adds an edge.\n"
"\n"
" If `src` or `dst` is not already in the graph, it is created by\n"
" calling the `by` function with the node ID to generate the node data.\n"
" Nodes that already exist are left unchanged.\n"
"\n"
" Always succeeds and returns a `Graph` (never fails).\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" // Nodes 1 and 2 are created automatically with value that's the same as NodeId\n"
" model.new(model.Directed)\n"
" |> model.add_edge_with(from: 1, to: 2, with: 10, by: fn(x) { x })\n"
" ```\n"
"\n"
" ```gleam\n"
" // Existing nodes keep their data; only missing ones get the default\n"
" model.new(model.Directed)\n"
" |> model.add_node(1, \"1\")\n"
" |> model.add_edge_with(from: 1, to: 2, with: 5, by: fn(n) { int.to_string(n) <> \":new\" })\n"
" // Node 1 is still \"1\", node 2 is \"2:new\"\n"
" ```\n"
).
-spec add_edge_with(
graph(DNJ, DNK),
integer(),
integer(),
DNK,
fun((integer()) -> DNJ)
) -> graph(DNJ, DNK).
add_edge_with(Graph, Src, Dst, Weight, By) ->
Graph@1 = ensure_node_with(Graph, Src, By),
Graph@2 = ensure_node_with(Graph@1, Dst, By),
add_edge_unchecked(Graph@2, Src, Dst, Weight).
-file("src/yog/model.gleam", 271).
?DOC(
" Adds multiple edges to the graph in a single operation.\n"
"\n"
" Fails fast on the first edge that references non-existent nodes.\n"
" Returns `Error` if any endpoint node doesn't exist.\n"
"\n"
" This is more ergonomic than chaining multiple `add_edge` calls\n"
" as it only requires unwrapping a single `Result`.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(graph) =\n"
" model.new(Directed)\n"
" |> model.add_node(1, \"A\")\n"
" |> model.add_node(2, \"B\")\n"
" |> model.add_node(3, \"C\")\n"
" |> model.add_edges([\n"
" #(1, 2, 10),\n"
" #(2, 3, 5),\n"
" #(1, 3, 15),\n"
" ])\n"
" ```\n"
).
-spec add_edges(graph(DNP, DNQ), list({integer(), integer(), DNQ})) -> {ok,
graph(DNP, DNQ)} |
{error, binary()}.
add_edges(Graph, Edges) ->
gleam@list:try_fold(
Edges,
Graph,
fun(G, Edge) ->
{Src, Dst, Weight} = Edge,
add_edge(G, Src, Dst, Weight)
end
).
-file("src/yog/model.gleam", 300).
?DOC(
" Adds multiple simple edges (weight = 1) to the graph.\n"
"\n"
" Fails fast on the first edge that references non-existent nodes.\n"
" Convenient for unweighted graphs where all edges have weight 1.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(graph) =\n"
" model.new(Directed)\n"
" |> model.add_node(1, \"A\")\n"
" |> model.add_node(2, \"B\")\n"
" |> model.add_node(3, \"C\")\n"
" |> model.add_simple_edges([\n"
" #(1, 2),\n"
" #(2, 3),\n"
" #(1, 3),\n"
" ])\n"
" ```\n"
).
-spec add_simple_edges(graph(DNY, integer()), list({integer(), integer()})) -> {ok,
graph(DNY, integer())} |
{error, binary()}.
add_simple_edges(Graph, Edges) ->
gleam@list:try_fold(
Edges,
Graph,
fun(G, Edge) ->
{Src, Dst} = Edge,
add_edge(G, Src, Dst, 1)
end
).
-file("src/yog/model.gleam", 329).
?DOC(
" Adds multiple unweighted edges (weight = Nil) to the graph.\n"
"\n"
" Fails fast on the first edge that references non-existent nodes.\n"
" Convenient for graphs where edges carry no weight information.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(graph) =\n"
" model.new(Directed)\n"
" |> model.add_node(1, \"A\")\n"
" |> model.add_node(2, \"B\")\n"
" |> model.add_node(3, \"C\")\n"
" |> model.add_unweighted_edges([\n"
" #(1, 2),\n"
" #(2, 3),\n"
" #(1, 3),\n"
" ])\n"
" ```\n"
).
-spec add_unweighted_edges(graph(DOG, nil), list({integer(), integer()})) -> {ok,
graph(DOG, nil)} |
{error, binary()}.
add_unweighted_edges(Graph, Edges) ->
gleam@list:try_fold(
Edges,
Graph,
fun(G, Edge) ->
{Src, Dst} = Edge,
add_edge(G, Src, Dst, nil)
end
).
-file("src/yog/model.gleam", 485).
?DOC(
" Removes a node and all its connected edges (incoming and outgoing).\n"
"\n"
" **Time Complexity:** O(deg(v)) - proportional to the number of edges\n"
" connected to the node, not the whole graph.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(graph) =\n"
" model.new(Directed)\n"
" |> model.add_node(1, \"A\")\n"
" |> model.add_node(2, \"B\")\n"
" |> model.add_node(3, \"C\")\n"
" |> model.add_edges([#(1, 2, 10), #(2, 3, 20)])\n"
"\n"
" let graph = model.remove_node(graph, 2)\n"
" // Node 2 is removed, along with edges 1->2 and 2->3\n"
" ```\n"
).
-spec remove_node(graph(DQR, DQS), integer()) -> graph(DQR, DQS).
remove_node(Graph, Id) ->
Targets = successors(Graph, Id),
Sources = predecessors(Graph, Id),
New_nodes = gleam@dict:delete(erlang:element(3, Graph), Id),
New_out = gleam@dict:delete(erlang:element(4, Graph), Id),
New_in_cleaned = begin
gleam@list:fold(
Targets,
erlang:element(5, Graph),
fun(Acc_in, _use1) ->
{Target_id, _} = _use1,
yog@internal@utils:dict_update_inner(
Acc_in,
Target_id,
Id,
fun gleam@dict:delete/2
)
end
)
end,
New_in = gleam@dict:delete(New_in_cleaned, Id),
New_out_cleaned = begin
gleam@list:fold(
Sources,
New_out,
fun(Acc_out, _use1@1) ->
{Source_id, _} = _use1@1,
yog@internal@utils:dict_update_inner(
Acc_out,
Source_id,
Id,
fun gleam@dict:delete/2
)
end
)
end,
{graph, erlang:element(2, Graph), New_nodes, New_out_cleaned, New_in}.
-file("src/yog/model.gleam", 550).
-spec do_remove_directed_edge(graph(DRD, DRE), integer(), integer()) -> graph(DRD, DRE).
do_remove_directed_edge(Graph, Src, Dst) ->
New_out = case gleam_stdlib:map_get(erlang:element(4, Graph), Src) of
{ok, Targets} ->
gleam@dict:insert(
erlang:element(4, Graph),
Src,
gleam@dict:delete(Targets, Dst)
);
{error, _} ->
erlang:element(4, Graph)
end,
New_in = case gleam_stdlib:map_get(erlang:element(5, Graph), Dst) of
{ok, Sources} ->
gleam@dict:insert(
erlang:element(5, Graph),
Dst,
gleam@dict:delete(Sources, Src)
);
{error, _} ->
erlang:element(5, Graph)
end,
{graph, erlang:element(2, Graph), erlang:element(3, Graph), New_out, New_in}.
-file("src/yog/model.gleam", 537).
?DOC(
" Removes a directed edge from `src` to `dst`.\n"
"\n"
" For **directed graphs**, this removes the single directed edge from `src` to `dst`.\n"
" For **undirected graphs**, this removes the edges in both directions\n"
" (from `src` to `dst` and from `dst` to `src`).\n"
"\n"
" **Time Complexity:** O(1)\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" // Directed graph - removes single directed edge\n"
" let assert Ok(graph) =\n"
" model.new(Directed)\n"
" |> model.add_node(1, \"A\")\n"
" |> model.add_node(2, \"B\")\n"
" |> model.add_edge(from: 1, to: 2, with: 10)\n"
" let graph = model.remove_edge(graph, 1, 2)\n"
" // Edge 1->2 is removed\n"
" ```\n"
"\n"
" ```gleam\n"
" // Undirected graph - removes both directions\n"
" let assert Ok(graph) =\n"
" model.new(Undirected)\n"
" |> model.add_node(1, \"A\")\n"
" |> model.add_node(2, \"B\")\n"
" |> model.add_edge(from: 1, to: 2, with: 10)\n"
" let graph = model.remove_edge(graph, 1, 2)\n"
" // Edge between 1 and 2 is fully removed\n"
" ```\n"
).
-spec remove_edge(graph(DQX, DQY), integer(), integer()) -> graph(DQX, DQY).
remove_edge(Graph, Src, Dst) ->
Graph@1 = do_remove_directed_edge(Graph, Src, Dst),
case erlang:element(2, Graph@1) of
directed ->
Graph@1;
undirected ->
do_remove_directed_edge(Graph@1, Dst, Src)
end.
-file("src/yog/model.gleam", 623).
-spec do_add_directed_combine(
graph(DRR, DRS),
integer(),
integer(),
DRS,
fun((DRS, DRS) -> DRS)
) -> graph(DRR, DRS).
do_add_directed_combine(Graph, Src, Dst, Weight, With_combine) ->
Update_fn = fun(Maybe_inner_map) -> case Maybe_inner_map of
{some, M} ->
New_weight = case gleam_stdlib:map_get(M, Dst) of
{ok, Existing} ->
With_combine(Existing, Weight);
{error, _} ->
Weight
end,
gleam@dict:insert(M, Dst, New_weight);
none ->
maps:from_list([{Dst, Weight}])
end end,
New_out = gleam@dict:upsert(erlang:element(4, Graph), Src, Update_fn),
New_in = gleam@dict:upsert(
erlang:element(5, Graph),
Dst,
fun(Maybe_m) -> case Maybe_m of
{some, M@1} ->
New_weight@1 = case gleam_stdlib:map_get(M@1, Src) of
{ok, Existing@1} ->
With_combine(Existing@1, Weight);
{error, _} ->
Weight
end,
gleam@dict:insert(M@1, Src, New_weight@1);
none ->
maps:from_list([{Src, Weight}])
end end
),
{graph, erlang:element(2, Graph), erlang:element(3, Graph), New_out, New_in}.
-file("src/yog/model.gleam", 593).
?DOC(
" Adds an edge, but if an edge already exists between `src` and `dst`,\n"
" it combines the new weight with the existing one using `with_combine`.\n"
"\n"
" The combine function receives `(existing_weight, new_weight)` and should\n"
" return the combined weight.\n"
"\n"
" Returns `Error` if either endpoint node doesn't exist in `graph.nodes`.\n"
"\n"
" **Time Complexity:** O(1)\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(graph) =\n"
" model.new(Directed)\n"
" |> model.add_node(1, \"A\")\n"
" |> model.add_node(2, \"B\")\n"
" |> model.add_edge(from: 1, to: 2, with: 10)\n"
" let assert Ok(graph) = model.add_edge_with_combine(graph, from: 1, to: 2, with: 5, using: int.add)\n"
" // Edge 1->2 now has weight 15 (10 + 5)\n"
" ```\n"
"\n"
" ## Use Cases\n"
"\n"
" - **Edge contraction** in graph algorithms (Stoer-Wagner min-cut)\n"
" - **Multi-graph support** (adding parallel edges with combined weights)\n"
" - **Incremental graph building** (accumulating weights from multiple sources)\n"
).
-spec add_edge_with_combine(
graph(DRJ, DRK),
integer(),
integer(),
DRK,
fun((DRK, DRK) -> DRK)
) -> {ok, graph(DRJ, DRK)} | {error, binary()}.
add_edge_with_combine(Graph, Src, Dst, Weight, With_combine) ->
case {gleam@dict:has_key(erlang:element(3, Graph), Src),
gleam@dict:has_key(erlang:element(3, Graph), Dst)} of
{true, true} ->
Graph@1 = do_add_directed_combine(
Graph,
Src,
Dst,
Weight,
With_combine
),
Result = case erlang:element(2, Graph@1) of
directed ->
Graph@1;
undirected ->
do_add_directed_combine(
Graph@1,
Dst,
Src,
Weight,
With_combine
)
end,
{ok, Result};
{false, false} ->
{error,
<<<<<<<<"Nodes "/utf8, (erlang:integer_to_binary(Src))/binary>>/binary,
" and "/utf8>>/binary,
(erlang:integer_to_binary(Dst))/binary>>/binary,
" do not exist"/utf8>>};
{false, _} ->
{error,
<<<<"Node "/utf8, (erlang:integer_to_binary(Src))/binary>>/binary,
" does not exist"/utf8>>};
{_, false} ->
{error,
<<<<"Node "/utf8, (erlang:integer_to_binary(Dst))/binary>>/binary,
" does not exist"/utf8>>}
end.