haskellffistate-monadst-monad

Re-dress a ST monad as something similar to the State monad


Here's the scenario: Given is a C library, with some struct at its core and operations thereon provided by an abundance of C functions.

Step 1: Using Haskell's FFI a wrapper is created. It has functions like myCLibInit :: IO MyCLibObj, myCLibOp1 :: MyCLibObj -> ... -> IO (), and so on. MyCLibObj is an opaque type that carries (and hides) a Ptr or ForeignPtr to the actual C struct, for example as shown in this wiki or in RWH ch. 17.

Step 2: Using unsafeIOToST from Control.Monad.ST.Unsafe convert all the IO actions into ST actions. This is done by introducing something like

 data STMyCLib s = STMyCLib MyCLibObj

and then wrapping all IO functions in ST functions, for example:

myCLibInit' :: ST s (STMyCLib s)
myCLibInit' = unsafeIOToST $ STMyCLib <$> myCLibInit

This allows to write imperative-style programs that mirror the use of the OO-like C library, e.g.:

doSomething :: ST s Bool
doSomething = do
    obj1 <- myCLibInit'
    success1 <- myCLibOp1' obj1 "some-other-input"
    ...
    obj2 <- myCLibInit'
    result <- myCLibOp2' obj2 42
    ...
    return True   -- or False

main :: IO ()
main = do
    ...
    let success = runST doSomething
    ...

Step 3: Often it doesn't make sense to mingle operations on several MyCLibObj in one do-block. For example when the C struct is (or should be thought of as) a singleton instance. Doing something like in doSomething above is either nonsensical, or just plain forbidden (for example, when the C struct is a static). In this case language resembling the one of the State monad is necessary:

doSomething :: ResultType
doSomething =  withMyCLibInstance $ do
    success <- myCLibOp1'' "some-other-input"
    result <- myCLibOp2'' 42
    ...
    return result

where

withMyCLibInstance :: Q a -> a

And this leads to the question: How can the ST s a monad be re-dressed as something that resembles more the State monad. Since withMyCLibInstance would use the runST function the new monad, let's call it Q (for 'q'uestion), should be

newtype Q a = Q (forall s. ST s a)

This looks thoroughly weird to me. I'm already struggling with implementing the Functor instance for this Q, let alone Applicative and Monad. ST s actually is a monad, already, but state s mustn't escape the ST monad, hence the forall s. ST s a. It's the only way to get rid of the s because runST :: (forall s. ST s a) -> a, and withMyCLibInstance is just a myCLibInit' followed by a runST. But somehow this doesn't fit.

What is the right way to tackle step 3? Should I even do step 2, or roll my Q right after step 1? My sense is that this should be quite simple. The ST monad has all I need, the Q just needs to be set up the right way...

Update 1: The singleton and static struct examples in step 3 are not very good. If two such do blocks were executed in parallel, very bad things might happen, i.e. both do blocks would work on the same C struct in parallel.


Solution

  • You can use a reader effect to access a singleton, instantiating it only in the run function:

    newtype MyCLibST s a = MyCLibST { unMyCLibST :: ReaderT (STMyCLib s) (ST s) a }
    
    runMyCLibST :: (forall s. MyCLibST s a) -> a
    runMyCLibST m = runST (myCLibInit >>= runReaderT (unMyCLibST m))
    
    -- Wrap the API with this.
    unsafeMkMyCLibST :: (MyCLibObj -> IO a) -> MyCLibST s a
    

    s should appear as a parameter to MyCLibST if you want to keep access to other ST features like mutable references and arrays.