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 StateT
around IO
is a bad idea. You just stumbled over one instance where that surfaces.
mystatefulcomp :: Cluster ()
mystatefulcomp = do
old_state <- get
receiveWait [ match doSomethingWithString ]
new_state <- get
The problem is what ends up in new_state
if doSomethingWithString
throws an error. The old_state
? Some intermediate state from doSomethingWithString
before the exception? You see, the very fact that we are wondering makes this approach no less bad than just storing the state in an IORef
or MVar
.
Apart from questionable semantics, this can't even be implemented without distributed-process
being rewritten to use MonadBaseControl
everywhere. This is exactly why distributed-process-lifted
fails to deliver, because it just wraps around the primitives from distributed-process
.
So, what I would do here instead is to pass around a data Config = Config { clusterState :: MVar ClusterState }
environment (Oh look, Process
does that, too!). Possibly with ReaderT
which interacts with IO
in a sane way, plus you can easily lift any number of nested occurences of Process
to ReaderT Config Process
yourself.
Repeating the message of Michael's blog posts: StateT
isn't bad in general (in a pure transformer stack, that is), just for cases where we wrap IO
in some way. I encourage you to read those posts, they were very inspiring for me, so here they are again: