Thicket data repository for the EEG
1{
2 "id": "https://www.tunbury.org/2025/06/23/transitive-reduction",
3 "title": "Transitive Reduction of Package Graph",
4 "link": "https://www.tunbury.org/2025/06/23/transitive-reduction/",
5 "updated": "2025-06-23T00:00:00",
6 "published": "2025-06-23T00:00:00",
7 "summary": "I have previously written about using a topological sort of a directed acyclic graph (DAG) of package dependencies to create an ordered list of installation operations. I now want to create a transitive reduction, giving a graph with the same vertices and the fewest number of edges possible.",
8 "content": "<p>I have previously written about using a <a href=\"https://www.tunbury.org/topological-sort/\">topological sort</a> of a directed acyclic graph (DAG) of package dependencies to create an ordered list of installation operations. I now want to create a transitive reduction, giving a graph with the same vertices and the fewest number of edges possible.</p>\n\n<p>This is interesting in opam, where a typical package is defined to depend upon both OCaml and Dune. However, Dune depends upon OCaml, so minimally the package only depends upon Dune. For opam, we would typically list both, as they may have version constraints.</p>\n\n<div><div><pre><code><span>depends</span><span>:</span> <span>[</span>\n <span>\"</span><span>dune\"</span> <span>{</span><span>></span><span>= \"3.17\"</span><span>}</span>\n <span>\"</span><span>ocaml\"</span>\n<span>]</span>\n</code></pre></div></div>\n\n<p>Given a topologically sorted list of packages, we can fold over the list to build a map of the packages and dependencies. As each package is considered in turn, it must either have no dependencies or the dependent package must already be in the map.</p>\n\n<div><div><pre><code><span>let</span> <span>pkg_deps</span> <span>solution</span> <span>=</span>\n <span>List</span><span>.</span><span>fold_left</span> <span>(</span><span>fun</span> <span>map</span> <span>pkg</span> <span>-></span>\n <span>let</span> <span>deps_direct</span> <span>=</span> <span>PackageMap</span><span>.</span><span>find</span> <span>pkg</span> <span>solution</span> <span>in</span>\n <span>let</span> <span>deps_plus_children</span> <span>=</span> <span>PackageSet</span><span>.</span><span>fold</span> <span>(</span><span>fun</span> <span>pkg</span> <span>acc</span> <span>-></span>\n <span>PackageSet</span><span>.</span><span>union</span> <span>acc</span> <span>(</span><span>PackageMap</span><span>.</span><span>find</span> <span>pkg</span> <span>map</span><span>))</span> <span>deps_direct</span> <span>deps_direct</span> <span>in</span>\n <span>PackageMap</span><span>.</span><span>add</span> <span>pkg</span> <span>deps_plus_children</span> <span>map</span><span>)</span> <span>PackageMap</span><span>.</span><span>empty</span><span>;;</span>\n</code></pre></div></div>\n\n<p>To generate the transitive reduction, take each set of dependencies for every package in the solution and remove those where the package is a member of the set of all the dependencies of any other directly descendant package.</p>\n\n<div><div><pre><code><span>let</span> <span>reduce</span> <span>dependencies</span> <span>=</span>\n <span>PackageMap</span><span>.</span><span>map</span> <span>(</span><span>fun</span> <span>u</span> <span>-></span>\n <span>PackageSet</span><span>.</span><span>filter</span> <span>(</span><span>fun</span> <span>v</span> <span>-></span>\n <span>let</span> <span>others</span> <span>=</span> <span>PackageSet</span><span>.</span><span>remove</span> <span>v</span> <span>u</span> <span>in</span>\n <span>PackageSet</span><span>.</span><span>fold</span> <span>(</span><span>fun</span> <span>o</span> <span>acc</span> <span>-></span>\n <span>acc</span> <span>||</span> <span>PackageSet</span><span>.</span><span>mem</span> <span>v</span> <span>(</span><span>PackageMap</span><span>.</span><span>find</span> <span>o</span> <span>dependencies</span><span>)</span>\n <span>)</span> <span>others</span> <span>false</span> <span>|></span> <span>not</span>\n <span>)</span> <span>u</span>\n <span>);;</span>\n</code></pre></div></div>\n\n<p>Let’s create a quick print function and then test the code:</p>\n\n<div><div><pre><code><span>let</span> <span>print</span> <span>=</span> <span>PackageMap</span><span>.</span><span>iter</span> <span>(</span><span>fun</span> <span>p</span> <span>deps</span> <span>-></span>\n <span>print_endline</span> <span>(</span><span>p</span> <span>^</span> <span>\": \"</span> <span>^</span> <span>(</span><span>PackageSet</span><span>.</span><span>to_list</span> <span>deps</span> <span>|></span> <span>String</span><span>.</span><span>concat</span> <span>\",\"</span><span>))</span>\n<span>);;</span>\n</code></pre></div></div>\n\n<p>The original solution is</p>\n\n<div><div><pre><code><span>#</span> <span>print</span> <span>dune</span><span>;;</span>\n<span>base</span><span>-</span><span>threads</span><span>.</span><span>base</span><span>:</span>\n<span>base</span><span>-</span><span>unix</span><span>.</span><span>base</span><span>:</span>\n<span>dune</span><span>:</span> <span>base</span><span>-</span><span>threads</span><span>.</span><span>base</span><span>,</span><span>base</span><span>-</span><span>unix</span><span>.</span><span>base</span><span>,</span><span>ocaml</span>\n<span>ocaml</span><span>:</span> <span>ocaml</span><span>-</span><span>config</span><span>,</span><span>ocaml</span><span>-</span><span>variants</span>\n<span>ocaml</span><span>-</span><span>config</span><span>:</span> <span>ocaml</span><span>-</span><span>variants</span>\n<span>ocaml</span><span>-</span><span>variants</span><span>:</span>\n<span>-</span> <span>:</span> <span>unit</span> <span>=</span> <span>()</span>\n</code></pre></div></div>\n\n<p>And the reduced solution is:</p>\n\n<div><div><pre><code><span>#</span> <span>let</span> <span>dependencies</span> <span>=</span> <span>pkg_deps</span> <span>dune</span> <span>(</span><span>topological_sort</span> <span>dune</span><span>);;</span>\n<span>val</span> <span>dependencies</span> <span>:</span> <span>PackageSet</span><span>.</span><span>t</span> <span>PackageMap</span><span>.</span><span>t</span> <span>=</span> <span><</span><span>abstr</span><span>></span>\n<span>#</span> <span>print</span> <span>(</span><span>reduce</span> <span>dependencies</span> <span>dune</span><span>);;</span>\n<span>base</span><span>-</span><span>threads</span><span>.</span><span>base</span><span>:</span>\n<span>base</span><span>-</span><span>unix</span><span>.</span><span>base</span><span>:</span>\n<span>dune</span><span>:</span> <span>base</span><span>-</span><span>threads</span><span>.</span><span>base</span><span>,</span><span>base</span><span>-</span><span>unix</span><span>.</span><span>base</span><span>,</span><span>ocaml</span>\n<span>ocaml</span><span>:</span> <span>ocaml</span><span>-</span><span>config</span>\n<span>ocaml</span><span>-</span><span>config</span><span>:</span> <span>ocaml</span><span>-</span><span>variants</span>\n<span>ocaml</span><span>-</span><span>variants</span><span>:</span>\n<span>-</span> <span>:</span> <span>unit</span> <span>=</span> <span>()</span>\n</code></pre></div></div>\n\n<p>This doesn’t look like much of a difference, but when applied to a larger graph, for example, 0install.2.18, the reduction is quite dramatic.</p>\n\n<p>Initial graph</p>\n\n<p><img alt=\"opam installation graph for 0install\" src=\"https://www.tunbury.org/images/0install-graph.png\"></p>\n\n<p>Transitive reduction</p>\n\n<p><img alt=\"Transitive reduction of the opam installation graph for 0install\" src=\"https://www.tunbury.org/images/0install-reduced-graph.png\"></p>",
9 "content_type": "html",
10 "author": {
11 "name": "Mark Elvers",
12 "email": "mark.elvers@tunbury.org",
13 "uri": null
14 },
15 "categories": [
16 "OCaml",
17 "tunbury.org"
18 ],
19 "source": "https://www.tunbury.org/atom.xml"
20}