Search code examples
haskellmonad-transformersstate-monadcloud-haskell

Distributed Process in monad transformer


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


Solution

  • 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:

    1. https://www.fpcomplete.com/blog/2017/06/readert-design-pattern
    2. https://www.fpcomplete.com/blog/2017/06/understanding-resourcet
    3. https://www.fpcomplete.com/blog/2017/06/tale-of-two-brackets
    4. https://www.fpcomplete.com/blog/2017/07/announcing-new-unliftio-library
    5. https://www.fpcomplete.com/blog/2017/07/the-rio-monad