Graphs are a natural way to represent systems of connections — from flight networks to dependencies in code or social networks. In directed graphs, edges have a direction, and not all nodes may be reachable from one another. This brings up a fascinating question: From which nodes can we reach all others?
To explore this, I wrote a Haskell function that recursively discovers all directed paths in a graph, and identifies the nodes that can reach every other node.
Given a directed graph represented as a pair ([nodes], [(from, to)])
,
we want to:
The function should therefore output a list of path groups — one for each fully reachable starting node.
findDirection :: (Eq a) => ([a], [(a, a)]) -> [[[a]]]
findDirection (nodexs, edgexs) =
let outxs = subFindDirection (nodexs, edgexs) (length nodexs)
newoutxs = filter (\(x1, _) -> x1) outxs
in map snd newoutxs
subFindDirection :: (Eq a) => ([a], [(a, a)]) -> Int -> [(Bool, [[a]])]
subFindDirection ([], _) _ = []
subFindDirection ((fstval:nodexs), edgexs) cmp =
let outxs = subFindDirection2 edgexs [fstval] fstval cmp
in [(cmp == (length . unique . concat $ outxs), outxs)]
++ subFindDirection (nodexs, edgexs) cmp
subFindDirection2 :: (Eq a) => [(a, a)] -> [a] -> a -> Int -> [[a]]
subFindDirection2 xs outxs n cmp
| length outxs == cmp = [outxs]
| otherwise =
let newxs = filter (\(x, _) -> x == n) xs
in if null newxs
then [outxs]
else concat $ map (\(_, x2) ->
subFindDirection2 xs (x2:outxs) x2 cmp) newxs
The algorithm performs a recursive depth-first traversal from each node in the graph.
The helper subFindDirection2
recursively follows every outgoing edge (n, x2)
,
building all possible directed paths:
x2
to the path.
In subFindDirection
, we check if a node has reached all others:
cmp == (length . unique . concat $ outxs)
This means the number of unique nodes visited in all paths equals the total number of nodes. If that’s true, the node is a fully connected origin.
The main function findDirection
then keeps only the fully connected nodes and returns
their complete list of traversal paths.
Consider this directed graph:
nodes = ['a','b','c']
edges = [('a','b'),('b','c'),('a','c')]
Calling:
findDirection (nodes, edges)
Produces:
[[["c","b","a"],["c","a"]]]
In plain words:
'a'
, we can reach everyone: a → b → c
and a → c
.'b'
or 'c'
, we can’t reach all nodes.
Therefore, the only “fully connected origin” is 'a'
.
Conceptually, this algorithm builds a kind of reachability matrix:
Start Node | Can Reach All? | Paths |
---|---|---|
a | ✅ Yes | [a→b→c, a→c] |
b | ❌ No | [b→c] |
c | ❌ No | [c] |
The output structure — [[[a]]]
— groups all paths by origin,
keeping only the nodes that can fully reach others.
Eq
type — not just characters or integers.This project is a great example of how recursion in Haskell can model not just tree structures, but also complex traversals like reachability in graphs. The algorithm mimics depth-first search but in a purely functional way.
What’s even more interesting is that it does more than just find paths — it analyzes which nodes are fully connected, a concept that relates to strongly connected components in graph theory.
From recursion to reachability — Haskell turns abstract graph logic into elegant, declarative exploration.