Skip to main content

src/graffeo_map.erl

-module(graffeo_map).
-moduledoc """
Tier-1 value backend: an immutable, maps-backed graph with dual
adjacency. Every operation returns a new graph; the original is
untouched. Labels on vertices, weight/metadata on edges.
""".

-behaviour(graffeo_backend).
-behaviour(graffeo_builder).

-export([
    new/0,
    add_vertex/2, add_vertex/3,
    add_edge/3, add_edge/4,
    edge_meta/3,
    vertex_label/2
]).

%% graffeo_builder callbacks (envelope-based)
-export([
    empty_like/1,
    build_add_vertex/2, build_add_vertex/3,
    build_add_edge/4
]).

-export([
    vertices/1,
    out_neighbours/2,
    in_neighbours/2,
    in_degree/2,
    out_degree/2,
    no_edges/1,
    no_vertices/1
]).

-record(gmap, {
    vs = #{} :: #{graffeo:vertex() => graffeo:label()},
    out = #{} :: #{graffeo:vertex() => #{graffeo:vertex() => graffeo:edge_meta()}},
    in = #{} :: #{graffeo:vertex() => #{graffeo:vertex() => graffeo:edge_meta()}}
}).

-doc "Opaque reference to the map-backed graph internals.".
-opaque ref() :: #gmap{}.
-export_type([ref/0]).

%%% --- graffeo_builder callbacks ---

-doc "Create a new, empty map-backed graph.".
-spec new() -> ref().
new() ->
    #gmap{}.

-doc "Add a vertex with the default label (`undefined`).".
-spec add_vertex(ref(), graffeo:vertex()) -> ref().
add_vertex(G, V) ->
    add_vertex(G, V, undefined).

-doc "Add a vertex with a label.".
-spec add_vertex(ref(), graffeo:vertex(), graffeo:label()) -> ref().
add_vertex(#gmap{vs = Vs, out = Out, in = In} = G, V, Label) ->
    G#gmap{
        vs = Vs#{V => Label},
        out = maps:merge(#{V => #{}}, Out),
        in = maps:merge(#{V => #{}}, In)
    }.

-doc "Add an edge with default metadata (`#{weight => 1}`).".
-spec add_edge(ref(), graffeo:vertex(), graffeo:vertex()) -> ref().
add_edge(G, From, To) ->
    add_edge(G, From, To, #{weight => 1}).

-doc "Add an edge with metadata (weight, label).".
-spec add_edge(ref(), graffeo:vertex(), graffeo:vertex(), graffeo:edge_meta()) -> ref().
add_edge(G0, From, To, Meta) ->
    G1 = ensure_vertex(G0, From),
    G2 = ensure_vertex(G1, To),
    #gmap{out = Out, in = In} = G2,
    OutFrom = maps:get(From, Out),
    InTo = maps:get(To, In),
    G2#gmap{
        out = Out#{From := OutFrom#{To => Meta}},
        in = In#{To := InTo#{From => Meta}}
    }.

%%% --- graffeo_backend callbacks ---

-doc "All vertices in the graph.".
-spec vertices(ref()) -> [graffeo:vertex()].
vertices(#gmap{vs = Vs}) ->
    maps:keys(Vs).

-doc "Vertices reachable from `V` via outgoing edges.".
-spec out_neighbours(ref(), graffeo:vertex()) -> [graffeo:vertex()].
out_neighbours(#gmap{out = Out}, V) ->
    maps:keys(maps:get(V, Out, #{})).

-doc "Vertices that reach `V` via incoming edges.".
-spec in_neighbours(ref(), graffeo:vertex()) -> [graffeo:vertex()].
in_neighbours(#gmap{in = In}, V) ->
    maps:keys(maps:get(V, In, #{})).

-doc "Number of incoming edges to `V`.".
-spec in_degree(ref(), graffeo:vertex()) -> non_neg_integer().
in_degree(#gmap{in = In}, V) ->
    map_size(maps:get(V, In, #{})).

-doc "Number of outgoing edges from `V`.".
-spec out_degree(ref(), graffeo:vertex()) -> non_neg_integer().
out_degree(#gmap{out = Out}, V) ->
    map_size(maps:get(V, Out, #{})).

-doc "Total number of edges in the graph.".
-spec no_edges(ref()) -> non_neg_integer().
no_edges(#gmap{out = Out}) ->
    maps:fold(fun(_V, Adj, Acc) -> Acc + map_size(Adj) end, 0, Out).

-doc "Total number of vertices in the graph.".
-spec no_vertices(ref()) -> non_neg_integer().
no_vertices(#gmap{vs = Vs}) ->
    map_size(Vs).

%%% --- Extra accessors ---

-doc "Get edge metadata between two vertices.".
-spec edge_meta(ref(), graffeo:vertex(), graffeo:vertex()) ->
    {ok, graffeo:edge_meta()} | error.
edge_meta(#gmap{out = Out}, From, To) ->
    case maps:find(From, Out) of
        {ok, Adj} -> maps:find(To, Adj);
        error -> error
    end.

-doc "Get the label of a vertex.".
-spec vertex_label(ref(), graffeo:vertex()) ->
    {ok, graffeo:label()} | error.
vertex_label(#gmap{vs = Vs}, V) ->
    maps:find(V, Vs).

%%% --- graffeo_builder callbacks (envelope-based) ---

-doc false.
-spec empty_like(graffeo:graph()) -> graffeo:graph().
empty_like(_G) ->
    graffeo:wrap_ref(graffeo_map, new()).

-doc false.
-spec build_add_vertex(graffeo:graph(), graffeo:vertex()) -> graffeo:graph().
build_add_vertex(G, V) ->
    build_add_vertex(G, V, []).

-doc false.
-spec build_add_vertex(graffeo:graph(), graffeo:vertex(), graffeo:label()) -> graffeo:graph().
build_add_vertex(G, V, Label) ->
    graffeo:add_vertex(G, V, Label).

-doc false.
-spec build_add_edge(graffeo:graph(), graffeo:vertex(), graffeo:vertex(), graffeo:edge_meta()) ->
    graffeo:graph().
build_add_edge(G, From, To, Meta) ->
    graffeo:add_edge(G, From, To, Meta).

%%% --- Internal ---

-spec ensure_vertex(ref(), graffeo:vertex()) -> ref().
ensure_vertex(#gmap{vs = Vs} = G, V) ->
    case maps:is_key(V, Vs) of
        true -> G;
        false -> add_vertex(G, V)
    end.