[英]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. 然后,您必须确保将
construct
和destruct
添加到所有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:
它可能有一些我没有考虑过的微妙错误,但它是迄今为止唯一符合所有原始标准的解决方案:
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: 这样做的好处是:
MyObject -> r
functions using whatever ordinary code you like, no monads required MyObject -> r
函数,不需要monad MyObject
in order to call such functions in the middle of other ordinary pure code, with the help of withMyObject
MyObject
,以便在withMyObject
的帮助下在其他普通纯代码的中间调用这些函数 withMyObject
withMyObject
时,不能忘记调用析构函数 MyObject
after calling the destructor on it 1 MyObject
1 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.