简体   繁体   English

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

[英]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: 我有Haskell代码需要与C库接口,有点像这样:

// 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 . 原始的FFI代码使用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 . 我正在寻找的是一种处理Haskell中的对象的一般方法,而不需要在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? 有没有一个很好的方法来实现这一目标?

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). 这基本上就是状态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

And then you just string operations together. 然后你就把操作串起来了。 Injecting them into the CLib monad will mean they're sequenced. 将它们注入CLib monad将意味着它们被测序。 Essentially, you're faking your own IO , in a more unsafe way since you can escape. 从本质上讲,你是以一种更加不安全的方式伪装自己的IO ,因为你可以逃脱。

Then you must ensure that you add construct and destruct to the end of all CLib chains. 然后,您必须确保将constructdestruct添加到所有CLib链的末尾。 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. 最后一个大的箍通过跳是确保当你unsafePerformIO无论是在construct ,它实际上得到评估。


Frankly, this is all kinda pointless since it already exists, battle proven in IO . 坦率地说,这一切都是毫无意义的,因为它已经存在,在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 : 如果您不想使用名称IO

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

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 摘要 - 库作为有状态monad导出,而不是泄漏的IO操作集
  • Safe - the user can embed this code in pure code without using unsafePerformIO and they can expect the result to be pure 安全 - 用户可以在不使用unsafePerformIO的情况下将此代码嵌入到纯代码中,并且可以期望结果是纯粹的

Unfortunately the implementation is a bit complicated. 不幸的是,实现有点复杂。

Eg 例如

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

c2hs file: 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#}

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

Disclaimer: I've never actually worked with C stuff from Haskell, so I am not speaking from experience here. 免责声明:我从来没有真正使用过Haskell的C语言,所以我不是根据这里的经验说话。

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. 将C ++构造函数/析构函数包装为IO操作。 withMyObject uses IO to sequence the constructor, calling the user-specified function, calling the destructor, and returning the result. withMyObject使用IO对构造函数进行排序,调用用户指定的函数,调用析构函数并返回结果。 It can then unsafePerformIO that entire do block (as opposed to the individual operations within it, which you've already cooking doesn't work). 然后它可以unsafePerformIO是整个do阻止(而不是在它的个人操作,而您也已经烹调不工作)。 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. 你也需要使用deepSeq (这就是NFData约束存在的原因),或者懒惰可能会延迟使用MyObject直到它被破坏为止。

The advantages of this is are: 这样做的好处是:

  1. You can write pure MyObject -> r functions using whatever ordinary code you like, no monads required 您可以使用您喜欢的任何普通代码编写纯MyObject -> r函数,不需要monad
  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 您可以决定构造一个MyObject ,以便在withMyObject的帮助下在其他普通纯代码的中间调用这些函数
  3. You can't forget to call the destructor when you use withMyObject 使用withMyObject时,不能忘记调用析构函数
  4. You can't use the MyObject after calling the destructor on it 1 在调用析构函数后,你不能使用MyObject 1
  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. 您的系统中只有一个(小)位置使用unsafePerformIO ,因此这是您唯一需要仔细担心的是否有正确的顺序来证明它是安全的。 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. 主要缺点是构造MyObject然后将其传递给几个不相关的函数有点尴尬。 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. 您必须将它们捆绑到一个函数中,该函数返回每个原始结果的元组,然后在其上使用withMyObject 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). 或者,如果您还单独公开构造函数和析构函数的IO版本,则用户可以选择使用这些版本,如果IO不如使包装函数传递给withMyObject那么withMyObject (但是用户可能会在释放后意外使用MyObject它,或忘记释放它)。


1 Unless you do something silly like use id as the MyObject -> r function. 1除非你做一些愚蠢的事情,比如使用id作为MyObject -> r函数。 Presumably there's no NFData MyObject instance though. 据推测,没有NFData MyObject实例。 Also that sort of error would tend to come from willful abuse rather than accidental misunderstanding. 此类错误往往来自故意滥用而非偶然的误解。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM