Reputation: 4771
Is there an idiomatic way to express following code in Haskell?
main :: IO ()
main = loop initState1 initState2 initState3
loop :: State1 -> State2 -> State3 -> IO ()
loop s1 s2 s3 = do
s1' <- runService1 s1
s2' <- runService2 s2
s3' <- runService3 s3
loop s1' s2' s3'
This code is very verbose so I probably doing something weird.
Upvotes: 8
Views: 522
Reputation: 27771
This solution is not more succinct, but solves an infelicity of the original formulation: the "unfolding" of each process is commingled with the "zipping together" of all processes. It would be nice if we could define each process independently, and combine them later as we saw fit.
We need the following auxiliary type:
newtype Iter = Iter (IO Iter)
unfoldIter :: (s -> IO s) -> s -> Iter
unfoldIter f s = Iter (unfoldIter f <$> f s)
runIter :: Iter -> IO ()
runIter (Iter action) = action >>= runIter
doNothingIter :: Iter
doNothingIter = unfoldIter return ()
zipIter :: Iter -> Iter -> Iter
zipIter (Iter action1) (Iter action2) =
Iter (zipIter <$> action1 <*> action2)
instance Monoid Iter where
mempty = doNothingIter
mappend = zipIter
Then loop
becomes:
loop :: State1 -> State2 -> State3 -> IO ()
loop s1 s2 s3 =
runIter $ unfoldIter runService1 s1
<> unfoldIter runService2 s2
<> unfoldIter runService3 s3
If we don't want to define our own auxiliary type, we could use a streaming library that provided a "zipping" operation, like for example streaming does.
Upvotes: 1
Reputation: 120711
I practice, you probably want to shove that state in a suitable state monad. The lens
library makes it easy to access that:
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens.TH
import Control.Monad.Trans.State
data AllState = AllState { _s₀ :: State0, _s₁ :: State1, _s₂ :: State2 }
makeLenses ''AllState
loop :: StateT AllState IO ()
loop = do
s₀ <~ runService0 <$> use s₀
s₁ <~ runService1 <$> use s₁
s₂ <~ runService2 <$> use s₂
loop
main = evalStateT loop $ AllState initState0 initState1 initState2
As such, this doesn't buy you very much over your original code, but it becomes much more convenient if you also give the runService
actions a suitable state-monad type:
runService0 :: StateT State0 IO ()
runService1 :: StateT State1 IO ()
runService2 :: StateT State2 IO ()
...then you can simply use the zoom
mechanism:
loop :: StateT AllState IO ()
loop = do
zoom s₀ runService0
zoom s₁ runService1
zoom s₂ runService2
loop
or as Gurkenglas suggests
loop = forever $ do
zoom s₀ runService0
zoom s₁ runService1
zoom s₂ runService2
Upvotes: 6
Reputation: 2317
main = fix (zipWithM ($) >=>)
[runService1, runService2, runService3]
[initState1 , initState2 , initState3 ]
Compare fix . (>>=) :: IO a -> IO b
, which is forever
.
Edit: This only works if State1
= State2
= State3
. If not, data-fix
allows:
main = fix (traverse unFix >=>)
[ana runService1 initState1, ana runService2 initState2, ana runService3 initState3]
Upvotes: 8