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.
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.