如何在haskell中封装对象构造函数和析构函数

时间:2013-11-19 04:27:32

标签: c++ c haskell

我有Haskell代码需要与C库接口,如下所示:

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

原始FFI代码使用unsafePerformIO将所有这些函数包装为纯函数。这导致了错误和不一致,因为操作的顺序未定义。

我正在寻找的是在Haskell中处理对象而不诉诸IO中的所有内容的一般方法。什么是好的是我可以做的事情:

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

有没有很好的方法来实现这个目标?

3 个答案:

答案 0 :(得分:6)

我们的想法是不断传递每个组件的接力棒,以强制按顺序评估每个组件。这基本上就是状态monad(IO实际上是一个奇怪的状态monad。有点)。

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

data Baton = Baton -- Hide the constructor!

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

然后你就把操作串起来了。将它们注入CLib monad将意味着它们被排序。从本质上讲,你是以一种更加不安全的方式伪造自己的IO,因为你可以逃脱。

然后,您必须确保将constructdestruct添加到所有CLib个链的末尾。这可以通过导出类似

的函数轻松完成
clib :: CLib a -> a
clib m = runCLib $ construct >> m >> destruct

要跳过的最后一个大箍就是确保当unsafePerformIO construct中的任何内容时,它实际上会得到评估。


坦率地说,这一切都是毫无意义的,因为它已经存在,在IO中证明了这一点。而不是整个精心设计的过程,只是

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

如果您不想使用名称IO

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

答案 1 :(得分:2)

我的最终解决方案。它可能有一些我没有考虑过的微妙错误,但它是目前唯一符合所有原始标准的解决方案:

  • 严格 - 所有操作都正确排序
  • 摘要 - 将库导出为有状态monad而不是泄漏的IO操作集
  • 安全 - 用户可以在不使用unsafePerformIO的情况下将此代码嵌入到纯代码中,并且可以期望结果是纯粹的

不幸的是,实现有点复杂。

E.g。

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

c2hs文件:

{-# 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#}

测试程序:

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

构建

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

答案 2 :(得分:0)

免责声明:我从来没有真正使用过来自Haskell的C语言,所以我不是根据这里的经验发言。

但让我想到的是写下这样的东西:

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

将C ++构造函数/析构函数包装为IO操作。 withMyObject使用IO对构造函数进行排序,调用用户指定的函数,调用析构函数并返回结果。然后可以unsafePerformIO整个do块(与其中已经烹饪的单个操作相反,它不起作用)。您也需要使用deepSeq(这就是NFData约束存在的原因),或者懒惰可能会推迟MyObject的使用,直到它被破坏为止。

这样做的好处是:

  1. 您可以使用您喜欢的任何普通代码编写纯MyObject -> r函数,不需要monad
  2. 您可以决定构建MyObject,以便在withMyObject
  3. 的帮助下,在其他普通纯代码的中间调用此类函数
  4. 使用withMyObject
  5. 时,不能忘记调用析构函数
  6. 在调用析构函数后,你不能使用MyObject 1
  7. 您的系统中只有一个(小)位置使用unsafePerformIO,因此这是您唯一需要仔细担心的是否有正确的顺序来证明它是否安全。还有一个地方你必须担心确保正确使用析构函数。
  8. 它基本上是“构造,使用,破坏”模式,其中“使用”步骤的细节被抽象为参数,因此每次需要使用该模式时,您都可以拥有单个实现。

    主要缺点是构造MyObject然后将其传递给几个不相关的函数有点尴尬。您必须将它们捆绑到一个函数中,该函数返回每个原始结果的元组,然后使用withMyObject。或者,如果您还单独公开构造函数和析构函数的IO版本,则用户可以选择使用IO比使包装函数传递给withMyObject更不尴尬(但随后用户在释放后可能会意外使用MyObject,或忘记释放它。

    1 除非你做一些愚蠢的事情,比如使用id作为MyObject -> r函数。据推测,虽然没有NFData MyObject实例。此类错误往往来自故意滥用而不是偶然的误解。