Just write a loop:

> let loop gs gu
>    | Just z <- find_obj gu usyms = do
>            ...
>            (gs', gu') <- handle_obj_ar ...
>            loop gs' gu'
>    | otherwise = return (gs,gu)
> (gs, gu) <- loop def undef

mfix is for when you have mutually recursive data but you want the IO operation to only execute once.  It's most useful when working with some sort of lazy data structure like a list, tree, or graph.

I can't come up with an example for IO off the top of my head, but for the ICFP contest this year I wrote a gate/wire embedded language which had code that looks like this:

sample wireIn = do
   rec
       (wireOut,a) <- gate (wireIn,d)
       (d,b) <- gate (a, b)
   return wireOut

which would create a circuit like this:

---in->[  ]------out-------->
  +-d->[  ]--a-->[  ]-d---+
  |         +-b->[  ]-b-+ |
  |         +-----------+ |
  +-----------------------+

This code translates to something like

sample wireIn = do
    (wireOut, _, _, _) <- mfix $ \(_, b, d) -> do
        (wireOut', a) <- gate (wireIn, d)
        (d', b') <- gate (a,b)
        return (wireOut', b', d')
    return wireOut'

The key is that gate was lazy in its arguments; the gate didn't care what its input wires were, it just needed them to exist at the time you asked for the entire circuit definition.  mfix says 'run the *effects* in this code once, but the *data* might be recursive'.

  -- ryan