grasevski
grasevski

Reputation: 3054

How do I encapsulate object constructors and destructors in haskell

I have Haskell code which needs to interface with a C library somewhat like this:

// MyObject.h
typedef struct MyObject *MyObject;
MyObject newMyObject(void);
void myObjectDoStuff(MyObject myObject);
//...
void freeMyObject(MyObject myObject);

The original FFI code wraps all of these functions as pure functions using unsafePerformIO. This has caused bugs and inconsistencies because the sequencing of the operations is undefined.

What I am looking for is a general way of dealing with objects in Haskell without resorting to doing everything in IO. What would be nice is something where I can do something like:

myPureFunction :: String -> Int
-- create object, call methods, call destructor, return results

Is there a nice way to achieve this?

Upvotes: 5

Views: 1209

Answers (3)

Ben
Ben

Reputation: 71485

Disclaimer: I've never actually worked with C stuff from Haskell, so I am not speaking from experience here.

But what springs to mind for me is to write something like:

withMyObject :: NFData r => My -> Object -> Constructor -> Params -> (MyObject -> r) -> r

You wrap the C++ constructor/destructor as IO operations. withMyObject uses IO to sequence the constructor, calling the user-specified function, calling the destructor, and returning the result. It can then unsafePerformIO that entire do block (as opposed to the individual operations within it, which you've already cooking doesn't work). You need to use deepSeq too (which is why the NFData constraint is there), or laziness could defer the use of the MyObject until after it's been destructed.

The advantages of this is are:

  1. You can write pure MyObject -> r functions using whatever ordinary code you like, no monads required
  2. You can decide to construct a MyObject in order to call such functions in the middle of other ordinary pure code, with the help of withMyObject
  3. You can't forget to call the destructor when you use withMyObject
  4. You can't use the MyObject after calling the destructor on it1
  5. There is only one (small) place in your system where you use unsafePerformIO, and therefore that's the only place you have to carefully worry about whether you've got the sequencing correct to justify that it's safe after all. There's also only one place you have to worry about making sure you use the destructor properly.

It's basically the "construct, use, destruct" pattern with the particulars of the "use" step abstracted out as a parameter so that you can has a single implementation cover every time you need to use that pattern.

The main disadvantage is that it's a bit awkward to construct a MyObject and then pass it to several unrelated functions. You have to bundle them up into a function that returns a tuple of each of the original results, and then use withMyObject on that. Alternatively if you also expose the IO versions of he constructor and destructor separately the user has the option of using those if IO is less awkward than making wrapper functions to pass to withMyObject (but then it's possible for the user to accidentally use the MyObject after freeing it, or forget to free it).


1 Unless you do something silly like use id as the MyObject -> r function. Presumably there's no NFData MyObject instance though. Also that sort of error would tend to come from willful abuse rather than accidental misunderstanding.

Upvotes: 0

grasevski
grasevski

Reputation: 3054

My final solution. It probably has subtle bugs that I haven't considered, but it is the only solution so far which has met all of the original criteria:

  • Strict - all operations are sequenced correctly
  • Abstract - the library is exported as a stateful monad rather than a leaky set of IO operations
  • Safe - the user can embed this code in pure code without using unsafePerformIO and they can expect the result to be pure

Unfortunately the implementation is a bit complicated.

E.g.

// Stack.h
typedef struct Stack *Stack;
Stack newStack(void);
void pushStack(Stack, int);
int popStack(Stack);
void freeStack(Stack);

c2hs file:

{-# LANGUAGE ForeignFunctionInterface, GeneralizedNewtypeDeriving #-}
module CStack(StackEnv(), runStack, pushStack, popStack) where
import Foreign.C.Types
import Foreign.Ptr
import Foreign.ForeignPtr
import qualified Foreign.Marshal.Unsafe
import qualified Control.Monad.Reader
#include "Stack.h"
{#pointer Stack foreign newtype#}

newtype StackEnv a = StackEnv
 (Control.Monad.Reader.ReaderT (Ptr Stack) IO a)
 deriving (Functor, Monad)

runStack :: StackEnv a -> a
runStack (StackEnv (Control.Monad.Reader.ReaderT m))
 = Foreign.Marshal.Unsafe.unsafeLocalState $ do
  s <- {#call unsafe newStack#}
  result <- m s
  {#call unsafe freeStack#} s
  return result

pushStack :: Int -> StackEnv ()
pushStack x = StackEnv . Control.Monad.Reader.ReaderT $
 flip {#call unsafe pushStack as _pushStack#} (fromIntegral x)

popStack :: StackEnv Int
popStack = StackEnv . Control.Monad.Reader.ReaderT $
 fmap fromIntegral . {#call unsafe popStack as _popStack#}

test program:

-- Main.hs
module Main where
import qualified CStack
main :: IO ()
main = print $ CStack.runStack x where
 x :: CStack.StackEnv Int
 x = pushStack 42 >> popStack

build:

$ gcc -Wall -Werror -c Stack.c
$ c2hs CStack.chs
$ ghc --make -Wall -Werror Main.hs Stack.o
$ ./Main
42

Upvotes: 2

daniel gratzer
daniel gratzer

Reputation: 53871

The idea is to keep passing a baton from each component to force each component to be evaluated in sequence. This is basically what the state monad is (IO is really a weird state monad. Kinda).

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import Control.Monad.State

data Baton = Baton -- Hide the constructor!

newtype CLib a = CLib {runCLib :: State Baton a} deriving Monad

And then you just string operations together. Injecting them into the CLib monad will mean they're sequenced. Essentially, you're faking your own IO, in a more unsafe way since you can escape.

Then you must ensure that you add construct and destruct to the end of all CLib chains. This is easily done by exporting a function like

clib :: CLib a -> a
clib m = runCLib $ construct >> m >> destruct

The last big hoop to jump through is to make sure that when you unsafePerformIO whatever's in construct, it actually gets evaluated.


Frankly, this is all kinda pointless since it already exists, battle proven in IO. Instead of this whole elaborate process, how about just

construct :: IO Object
destruct  :: IO ()
runClib :: (Object -> IO a) -> a
runClib = unsafePerformIO $ construct >>= m >> destruct

If you don't want to use the name IO:

newtype CLib a = {runCLib :: IO a} deriving (Functor, Applicative, Monad)

Upvotes: 5

Related Questions