pnevyk / gryf

Graph data structure library aspiring to be convenient, versatile, correct and performant.
MIT License
69 stars 1 forks source link

Algorithm configuration builders #20

Closed pnevyk closed 1 year ago

pnevyk commented 1 year ago

Instead of using several run methods with many arguments, the algorithms can use the builder pattern with reasonable defaults instead. Example with ShortestPaths:

Current way

ShortestPaths::run(&graph, start, Some(goal), edge_weight);
ShortestPaths::run_algo(&graph, start, Some(goal), edge_weight, Some(shortest_paths::Algo::Dijkstra));
ShortestPaths::run_dijkstra(&graph, start, Some(goal), edge_weight);
ShortestPaths::run_bellman_ford(&graph, start, edge_weight);

Builder pattern

The API is just a quick, first draft and is subject to change.

ShortestPaths::run(&graph, start); // with goal = None, edge_weight = identity, algo = any
ShortestPaths::with(&graph, start).goal(goal).edge_weight(edge_weight).dijkstra().run();

An interesting challenge will be to allow to choose a specific algorithm which should also result in more specific constraints on the graph (for which there are now separate functions such as run_dijkstra). But this should be quite doable with type parameters and zero-sized new types.

The main advantages are:

pnevyk commented 1 year ago

Alternative builder API:

ShortestPaths::on(&graph).run(start); // with goal = None, edge_weight = identity, algo = any
ShortestPaths::on(&graph).goal(goal).edge_weight(edge_weight).dijkstra().run(start);

The difference between on and run methods is:

The advantage is that the appearance of the usage is consistent whether or not any builder method is used. Although there could still be a shortcut:

ShortestPaths::run(&graph, start); // with goal = None, edge_weight = identity, algo = any
pnevyk commented 1 year ago

When implementing the run method on the builder, specify the graph constraints on the method level rather than impl level, because it arguably gives slightly better error messages when violated (playground).

// Method level - GOOD
impl<G> Builder<G, Dijkstra> {
    pub fn run(self, ...)
    where
        G: ...,
    {
        // ...
    }
}

// Impl level - WORSE
impl<G> Builder<G, BellmanFord>
where
    G: ...,
{
    pub fn run(self, ...) {
        // ...
    }
}