Feeding stateful and stateful-effectful computations into each other I am writing code that runs a simulation in an interactive way. The building blocks are:
runStep :: I -&gt; State S O
runWorld :: World m -&gt; O -&gt; StateT W m I

Note that the world has some state W but is also allowed to have effects of type m. My question is about the function that feeds runStep and runWorld into each other. My current implementation is quite heavy on the Control.Lens noise:
sim :: (Monad m) =&gt; World m -&gt; StateT (I, S, O) m Result
sim w = do
  inp &lt;- use _1
  out &lt;- zoom _2 $ state . runState $ runStep inp
  inp' &lt;- zoom _3 $ runWorld w out
  _1 .= inp'
  return $ resultComputedPurelyFrom out

What I'd like to improve on this code is two things:

Reduce the lens piping. I could, of course, get rid of _1, _2 and _3 by using a bespoke record type, but then that introduces its own noise -- sim is the only function that uses this particular combination of I, S and O.

The write-back to inp' could easily be accidentally omitted, resulting in not-obviously-wrong-looking, well-typed code that wouldn't work correctly.


ETA: An alternative formulation is to forego the lens stuff completely,  at the cost of making even the updating of the S component something that can be accidentally omitted:
sim :: (Monad m) =&gt; World m -&gt; StateT (I, S) (StateT O m) Result
sim w = do
  (inp, s) &lt;- get 
  let (out, s') = runState (runStep inp) s
  inp' &lt;- lift $ runWorld w out
  put (inp', s')
  return $ resultComputedPurelyFrom out
What I'd like to improve on this code is two things:

Reduce the lens piping. I could, of course, get rid of _1, _2 and _3 by using a bespoke record type, but then that introduces its own noise -- sim is the only function that uses this particular combination of I, S and O.


Is this really a problem? I don't understand what could be improved about that part.


The write-back to inp' could easily be accidentally omitted, resulting in not-obviously-wrong-looking, well-typed code that
wouldn't work correctly.


You can move the state-passing of I into a separate function for which that's the only job:
withInput :: ALens' s i -&gt; (i -&gt; StateT s i) -&gt; StateT s ()
withInput l go = do
  i &lt;- use (cloneLens l)
  i' &lt;- go i
  cloneLens l := i'

...
sim = do
  withInput _1 $ \inp -&gt;
    inp' &lt;- ...
    pure inp'
  ...

The result type () of withInput makes it difficult to forget to update the state: the function would fail to typecheck if you forget the last clause.