Im toying with implementing a gossip based cluster membership backend for the so called cloud-haskell or is it Distributed.Process.. anyway Im trying to get away with handeling state without ioref or MVars and instead using a state transformer and putting the Process monad on the bottom, like so:
type ClusterT = StateT ClusterState
type Cluster a = ClusterT Process a
This works fairly well using Control.Distributed.Process.Lifted (https://hackage.haskell.org/package/distributed-process-lifted) allowing you to do something like this:
mystatefulcomp :: Cluster ()
mystatefulcomp = do
msg <- expect :: Cluster String
old_state <- get
say $ "My old state was " ++ (show old_state)
put $ modifyState curr_state msg
mystatefulcomp
main = do
Right transport <- createTransport '127.0.0.1' '3000' (\n -> ('127.0.0.1', n) defaultTCPParameters
node <- newLocalNode transport initRemoteTable
runProcess node (evalStateT mystatefulcomp initialstate)
where initialstate = ClusterState.empty
this works resonably well and allows me to structure my program fairly well, i can keep my state functional and thread it along in the Cluster monad.
This all break tho when i try to use receiveWait and match to receive messages.
lets rewrite statefulcomp to do something else using receiveWait
doSomethingWithString :: String -> Cluster ()
doSomethingWithString str = do
s < get
put $ modifyState s str
mystatefulcomp :: Cluster ()
mystatefulcomp = do
old_state <- get
receiveWait [ match doSomthingWithString ]
new_state <- get
say $ "old state " ++ (show old_state) ++ " new " ++ (show new_state)
This wont work since the match function is of type (a -> Process b) -> Match b but we want it to be of type (a -> Cluster b) -> Match b. And here is where i get out on thin ice. As i understand Control.Distributed.Process.Lifted rexposes Control.Distributed.Process functions lifted into the tansformer stack allowing you to use functions like expect and say but does not rexposes match, matchIf and so on..
Im really struggeling with this trying to find a work around or a way of re implementing match and its friends to the form of MonadProcess m => (a -> m b) -> Match b.
Any insights is apriciated.
edit
So after som fiddeling about I came up with the following
doSomethingWithString :: String -> Cluster ()
doSomethingWithString str = do
s < get
put $ modifyState s str
doSomethingWithInt :: Int -> Cluster ()
...
mystatefulcomp :: Cluster ()
mystatefulcomp = do
old_state <- get
id =<< receiveWait [ match $ return . doSomethingWithString
, match $ return . doSomethingWithInt ]
new_state <- get
say $ "old state " ++ (show old_state) ++ " new " ++ (show new_state)
This works fairly well but I am still curious about how good of a design this is
As Michael Snoyman points out in a series of blog posts (that's 5 links), wrapping
StateTaroundIOis a bad idea. You just stumbled over one instance where that surfaces.The problem is what ends up in
new_stateifdoSomethingWithStringthrows an error. Theold_state? Some intermediate state fromdoSomethingWithStringbefore the exception? You see, the very fact that we are wondering makes this approach no less bad than just storing the state in anIOReforMVar.Apart from questionable semantics, this can't even be implemented without
distributed-processbeing rewritten to useMonadBaseControleverywhere. This is exactly whydistributed-process-liftedfails to deliver, because it just wraps around the primitives fromdistributed-process.So, what I would do here instead is to pass around a
data Config = Config { clusterState :: MVar ClusterState }environment (Oh look,Processdoes that, too!). Possibly withReaderTwhich interacts withIOin a sane way, plus you can easily lift any number of nested occurences ofProcesstoReaderT Config Processyourself.Repeating the message of Michael's blog posts:
StateTisn't bad in general (in a pure transformer stack, that is), just for cases where we wrapIOin some way. I encourage you to read those posts, they were very inspiring for me, so here they are again: