I am writing code that runs a simulation in an interactive way. The building blocks are:
runStep :: I -> State S O
runWorld :: World m -> O -> 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) => World m -> StateT (I, S, O) m Result
sim w = do
inp <- use _1
out <- zoom _2 $ state . runState $ runStep inp
inp' <- 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 ofI
,S
andO
.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) => World m -> StateT (I, S) (StateT O m) Result
sim w = do
(inp, s) <- get
let (out, s') = runState (runStep inp) s
inp' <- lift $ runWorld w out
put (inp', s')
return $ resultComputedPurelyFrom out