Home > Back-end >  What is the motivation behind the API of the Boost Graph DFS (and other algorithms)?
What is the motivation behind the API of the Boost Graph DFS (and other algorithms)?

Time:12-24

General question

As I am making my first steps into the BGL, I struggle understand what justifies the Principle of Least Astonishment being a bit bullied here.

Background

So, after lots of efforts I could build a tree graph, and I was expecting to be able to write something like

tree.dfs(visitor);

Now that I know that the BGL was not meant as an object-oriented API, I guess it makes sense to have instead to write something like

depth_first_search(tree, visitor)

Then since I learned that there is no Tree class in the BGL, that the design choice was to make the presence of a root a property of value and not a property of type. So I guess it explains why we would have to pass the root descriptor, something like

depth_first_search(graph_that_is_a_tree, visitor, root);

What confuses me

Until this point I have the feeling I follow (more or less) the design. But then to make my code compile I actually have to write:

  auto [root, tree] = my_stuff::to_k_ary_tree<my_vertex, my_edge>(ast);

  auto indexmap = boost::get(boost::vertex_index, tree);
  auto colormap = boost::make_vector_property_map<boost::default_color_type>(indexmap);

  MyVisitor<my_stuff::k_ary_tree<my_vertex,my_edge>> vis;
  std::vector<boost::default_color_type> colors(num_vertices(tree));
  boost::depth_first_search(tree, vis, colormap, root);

And this is where I lose it (the intuition): even if I have been warned a colormap would be needed for most of the algorithms, I do not understand/appreciate why I need this signature.

My question

Why can't the DFS algorithm be responsible for handling these details?

What I can think of

  • Is it because if tree is not a type? If there is no type distinction between Tree and Graph then there is no chance that a DFS could know that there is no cycle, and so it needs to color the vertices that have been explored for all cases - even when you are absolutely sure it's a tree, with a root, and no cycle.
  • If so, then it does not fully explain to me why the DFS can't create/access/destroy this color map without us knowing.

CodePudding user response:

The best explanation is right on the landing page in the documentation: https://www.boost.org/doc/libs/1_81_0/libs/graph/doc/index.html

It contrasts Genericity in STL [sic] versus Genericity in the Boost Graph Library. The most central blurb from the second section:

[First,] the graph algorithms of the BGL are written to an interface that abstracts away the details of the particular graph data-structure. Like the STL, the BGL uses iterators to define the interface for data-structure traversal. There are three distinct graph traversal patterns: traversal of all vertices in the graph, through all of the edges, and along the adjacency structure of the graph (from a vertex to each of its neighbors). There are separate iterators for each pattern of traversal.

This generic interface allows template functions such as breadth_first_search() to work on a large variety of graph data-structures, from graphs implemented with pointer-linked nodes to graphs encoded in arrays. This flexibility is especially important in the domain of graphs. Graph data-structures are often custom-made for a particular application. Traditionally, if programmers want to reuse an algorithm implementation they must convert/copy their graph data into the graph library's prescribed graph structure. This is the case with libraries such as LEDA, GTL, Stanford GraphBase; it is especially true of graph algorithms written in Fortran. This severely limits the reuse of their graph algorithms.

In contrast, custom-made (or even legacy) graph structures can be used as-is with the generic graph algorithms of the BGL, using external adaptation (see Section How to Convert Existing Graphs to the BGL). External adaptation wraps a new interface around a data-structure without copying and without placing the data inside adaptor objects. The BGL interface was carefully designed to make this adaptation easy. To demonstrate this, we have built interfacing code for using a variety of graph structures (LEDA graphs, Stanford GraphBase graphs, and even Fortran-style arrays) in BGL graph algorithms.

But Wait, There's More!

Related, Boost Geometry has a very similar Design Rationale section that goes into a lot more detail on how a generic template library design evolves into the form that BGL also has.

It does so by example, which is very nice, even though the examples are from the domain of computational geometry, of course.

Auxiliary Property Maps

Regarding the colormap (and other auxiliary algorithm state), I'll add:

  • you can default them either

    • using tagged properties (property<vertex_color_t, default_color_type>)
    • using trait specializations to associate your external maps with your graph type (specializing graph_property<>)
  • having them caller supplied makes it possible to have enormous efficiency gains when re-using the coloring/storage across algorithm invocations

  • Note most algorithms default the aux property maps. Indeed, DFS also does!

Simplified Sample

Partly based on your previous question code, here's a sample that shows how you can simplify the invocation.

Note that it uses ADL to advantage to avoid boost:: qualifying function names, and uses the named parameters overload of dfs:

Live On Coliru

#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/graph_utility.hpp>
#include <boost/graph/random.hpp>
#include <iomanip>
#include <random>

struct MyVisitor : boost::default_dfs_visitor {
    auto discover_vertex(auto u, auto const& g) {
        std::cout << "Discover vertex " << u << " (" << g[u] << ")\n";
    }
};

template <class V, class E>
    requires std::constructible_from<V, std::string> && std::constructible_from<E, double>
auto foo(...) {
    using G = boost::adjacency_list<boost::setS, boost::vecS, boost::directedS, V, E>;

    G g;
    std::mt19937 prng{std::random_device{}()};
    generate_random_graph(g, 10, 30, prng);

    for (auto e : make_iterator_range(edges(g)))
        g[e] = {std::uniform_real_distribution(1.0, 2.0)(prng)};

    for (int i = 0; i < 10;   i)
        g[i] = {std::array{"zero", "one", "two", "three", "four", "five", "six", "seven",
                           "eight", "nine"}
                    .at(i)};

    return std::tuple(std::move(g), 3);
}

struct V { std::string  name; };
struct E { double length; };
static inline auto& operator<<(std::ostream& os, V const& v) { return os << quoted(v.name); }

int main() {
    auto [g, root] = foo<V, E>();
    depth_first_search(g, visitor(MyVisitor{}));
    print_graph(g);
}

Prints, e.g.

Discover vertex 0 ("zero")
Discover vertex 5 ("five")
Discover vertex 1 ("one")
Discover vertex 2 ("two")
Discover vertex 4 ("four")
Discover vertex 6 ("six")
Discover vertex 8 ("eight")
Discover vertex 3 ("three")
Discover vertex 7 ("seven")
Discover vertex 9 ("nine")
0 --> 5 6 7 
1 --> 0 2 7 
2 --> 1 4 5 6 8 
3 --> 0 4 8 
4 --> 1 2 6 
5 --> 0 1 2 7 
6 --> 2 5 8 
7 --> 2 5 
8 --> 0 3 
9 --> 3 7 
  • Related