Feedback on simple DFS
The small things

    s &lt;- get
    let candidates = fst s
    let visited = snd s


could be simplified to

    (candidates, visited) &lt;- get


(It is technically a little bit stricter but it's not a problem because you don't rely on laziness here.)

    let ret = blah in
    ret


is equivalent to

    blah


At the end of dfsGetNext you return either 0 or 1, which is strange because the return type is Vertex, and although it is a synonym for Int (so this type checks), 0 and 1 don't look like vertices.

My guess is that you intended either:


to return a boolean to indicate whether the DFS ended, so Bool would be a more appropriate type;
to return the visited vertex, so, rather than returning dummy sentinel values, Maybe Vertex would be a more appropriate type.


Assuming that 0 and 1 are meant to be booleans, and with the other changes above, we get:

-- Also isolating the monad, and applying it to the result type in the function instead
type DFS = ReaderT Env (StateT DFSState Identity)

dfsGetNext :: DFS Bool
dfsGetNext = do
    graph &lt;- ask
    (candidates, visited) &lt;- get
    case candidates of
       []     -&gt; return False
       (x:xs) -&gt; if elem x visited 
                 then put (xs, visited) &gt;&gt; return True
                 else put ((getNeighbors x graph) ++ xs, visited ++ [x]) &gt;&gt; return True


Getting fancy

There's a more direct implementation of DFS. To do a DFS starting from a vertex v:


If v has already been visited, stop; otherwise...
Visit v
Recursively run a DFS from each neighbor of v


In particular, this description doesn't require you to explicitly keep track of "pending candidate vertices", nor to iterate a "single step" function like dfsGetNext; the DFS is directly defined.

-- The state now only contains the visited vertices, not the candidates (which is implicit in the recursive structure of dfsFrom)
type DFS = ReaderT Graph (State Visited)

dfsFrom :: Vertex -&gt; DFS ()
dfsFrom v = do
  visited &lt;- get
  if v `elem` visited then                     -- 1
    pure ()
  else do
    put (visited ++ [v])                       -- 2
    graph &lt;- ask
    let neighbors = getNeighbors v graph
    for_ neighbors dfsFrom                     -- 3


Now you can "run" that DFS, interpreting it as a function as follows:

-- dfs from 0
dfs :: Graph -&gt; [Vertex]
dfs graph = dfsFrom 0 `runReaderT` graph `execState` []


Your "single step" version does have one distinguishing feature, which is that you can easily interrupt the DFS at any time to inspect the state for example. But you can also do that with the recursive version, by passing a function (visit below) that will be called when visiting a node. Clients of the DFS are free to define it however they wish.

It's also useful to generalize the DFS monad to support additional effects relevant to users rather than to the DFS itself. This is straightforward since you're already using monad transformers, by changing the base monad from Identity to something else. Here I chose IO for simplicity and concreteness, but it might as well be a type parameter m.

type DFSIO = ReaderT Graph (StateT Visited IO)

dfsFromIO :: (Vertex -&gt; DFSIO ()) -&gt; Vertex -&gt; DFSIO ()
dfsFromIO visit v = do
  visited &lt;- get
  if v `elem` visited then
    pure ()
  else do
    visit v
    put (visited ++ [v])
    graph &lt;- ask
    let neighbors = getNeighbors v graph
    for_ neighbors (dfsFromIO visit)


Another possibility is to call visit at the very start of the function, before checking whether the vertex was visited, or at the very end, on the neighboring vertices, where the function could also have access to the cost of the edges being crossed.

Running this new version dfsFromIO is as straightforward as running dfsFrom (only replacing execState with execStateT since the base monad is no longer Identity):

-- dfs from 0
dfsIO :: (Vertex -&gt; DFSIO ()) -&gt; Graph -&gt; IO [Vertex]
dfsIO visit graph = dfsFromIO visit 0 `runReaderT` graph `execStateT` []


For example you can print every visited vertex as follows:

main :: IO ()
main = void (dfsIO (\v -&gt; liftIO (print v)))




Full gist: https://gist.github.com/Lysxia/4733f8d670c84e5b886f74d2d106d1f5