I have a directed graph that is cyclic in general, and I want to find relationships between nodes and sinks. Traversals over directed graphs seem to be well-suited for memoization with recursion, since the graph can be mapped to the results of the traversal at each node and queried, akin to the canonical Fibonacci memoization. However, I've found that detecting cycles thwarts the memoization effort, because cycle detection requires knowledge of the path, and there can be many paths leading to the same node, so the result mapped to the graph seems to depend on the path argument. However, when the cycle is ignored, the result is actually unambiguous, but I don't see any way to communicate this to the compiler.
As an simple but illustrative example, suppose I want to find all sinks reachable from a node in the graph. The naive algorithm with DFS looks something like this:
import qualified Data.Set as Set
type AdjList = [[Int]]
adj :: AdjList
adj = ...
sinks :: Int -> [Int]
sinks = sinks' Set.empty where
sinks' :: Set.Set Int -> Int -> [Int]
sinks' path node | node `Set.member` path = [] -- this forms a cycle
| null (adj !! node) = [node] -- this is a sink
| otherwise = concatMap (sinks' (Set.insert node path)) (adj !! node)
Whereas trying to write this for memoization clearly runs into a problem trying to fill in the path argument:
sinks = sinks' Set.empty where
sinks' path = (map (sinks'' {- what goes here? -}) [0..(length adj - 1)] !!) where
sinks'' path' node | node `Set.member` path' = [] -- this forms a cycle
| null (adj !! node) = [node] -- this is a sink
| otherwise = concatMap (sinks' (Set.insert node path')) (adj !! node)
Over a traversal on this graph for example, we can see how the paths to the cycle from A differ, but if the result from node D were memoized, the traversal past D would only have to be performed once:



Am I forced to write this memoization manually with an explicit table?