如何在haskell中封装对象构造函数和析构函数
How do I encapsulate object constructors and destructors in 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
有什么好方法可以实现这一点吗?
我们的想法是不断从每个组件传递接力棒,迫使每个组件按顺序进行评估。这基本上就是monad的状态(IO
实际上是一个奇怪的状态monad.Kinda)。
{-# 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
,因为你可以逃跑,所以这是一种更不安全的方式。
然后必须确保将construct
和destruct
添加到所有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)
我的最终解决方案。它可能有一些我没有考虑过的细微错误,但它是迄今为止唯一符合所有原始标准的解决方案:
- 严格-所有操作顺序正确
- 摘要-库被导出为一个有状态的monad,而不是一组泄漏的IO操作
- 安全-用户可以在不使用unsafePerformIO的情况下将此代码嵌入纯代码中,并且他们可以期望结果是纯的
不幸的是,实现有点复杂。
例如
// 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
免责声明:我从未真正使用过Haskell的C语言,所以我在这里不是根据经验说话。
但我脑海中浮现的是写这样的东西:
withMyObject :: NFData r => My -> Object -> Constructor -> Params -> (MyObject -> r) -> r
您将C++构造函数/析构函数包装为IO操作。withMyObject
使用IO对构造函数进行排序,调用用户指定的函数,调用析构函数,并返回结果。然后它可以unsafePerformIO
整个do
块(与其中的单个操作相反,您已经烹饪过的操作不起作用)。您也需要使用deepSeq
(这就是NFData
约束存在的原因),否则懒惰可能会将MyObject
的使用推迟到它被破坏之后。
这样做的优点是:
- 您可以使用任何您喜欢的普通代码编写纯
MyObject -> r
函数,不需要monad - 在
withMyObject
的帮助下,您可以决定构造一个MyObject
,以便在其他普通纯代码的中间调用这些函数 - 使用
withMyObject
时,不能忘记调用析构函数 - 在调用
MyObject
上的析构函数后不能使用它1 - 在你的系统中,只有一个(小)地方可以使用
unsafePerformIO
,因此,这是你唯一需要小心担心的地方,你是否有正确的测序来证明它是安全的。你只需要担心一个地方,那就是确保正确使用析构函数
它基本上是"构造、使用、销毁"模式,其中"使用"步骤的细节被抽象为一个参数,这样每次需要使用该模式时都可以有一个单独的实现覆盖。
主要的缺点是构造一个MyObject
然后将其传递给几个不相关的函数有点尴尬。您必须将它们捆绑到一个函数中,该函数返回每个原始结果的元组,然后在其中使用withMyObject
。或者,如果您还分别公开构造函数和析构函数的IO
版本,则如果IO
比使包装函数传递给withMyObject
不那么尴尬,则用户可以选择使用这些版本(但用户可能会在释放MyObject
后意外使用它,或者忘记释放它)。
1除非您做了一些愚蠢的事情,比如使用id
作为MyObject -> r
函数。假设没有NFData MyObject
实例。此外,这种错误往往来自故意的滥用,而不是意外的误解。
- 类中的 Arduino 对象构造函数设置垃圾值
- 编译错误:临时对象构造函数中缺少参数
- 双指针在使用 new 时不调用对象构造函数
- 以支持继承的方式将自身shared_ptr添加到对象构造函数中的向量中
- 在创建对象向量时,不为每个对象唯一调用默认对象构造函数
- GCC __attribute__((constructor)) 在对象构造函数之前调用
- 自定义对象构造函数在循环外部循环
- 从全局对象构造函数停止监视器计时器
- Arduino 上的 Sketch 停止在对象构造函数中执行
- 通过在引用线程对象来传递取消引用的“this”指针来在函数对象构造函数中创建线程是好是坏
- 为什么当对象构造函数投入新表达式时,为什么不调用DealLocation函数
- 临时结构对象构造函数奇数调用
- 未显式引用对象的全局对象构造函数在最终二进制文件 - LD 中被丢弃
- 为什么 Clang++ 不在另一个静态库中运行全局对象构造函数?
- 在未加载上下文的情况下在对象构造函数中使用OpenGL函数
- 为什么我们需要一个用户提供的const对象构造函数
- 对象构造函数的C++数组
- 将子结构值设置为对象构造函数中安全的纯虚拟函数返回的值
- C等价于新对象(构造函数)
- C++:直接在参数列表中使用数组文字的对象构造函数