如何使用C#(DLLImport)的高阶类型签名使用和调用Haskell函数,例如...
double :: (Int -> Int) -> Int -> Int -- higher order function
typeClassFunc :: ... -> Maybe Int -- type classes
data MyData = Foo | Bar -- user data type
dataFunc :: ... -> MyData
C#中对应的类型签名是什么?
[DllImport ("libHSDLLTest")]
private static extern ??? foo( ??? );
另外(因为可能更容易):如何在C#中使用“未知” Haskell类型,以便至少可以在C#不知道任何特定类型的情况下传递它们?我需要知道的最重要的功能是传递类型类(例如Monad或Arrow)。
我已经知道how to compile a Haskell library to DLL并在C#中使用,但仅用于一阶函数。我也知道Stackoverflow - Call a Haskell function in .NET,Why isn't GHC available for .NET和hs-dotnet,在这些地方我没有找到任何文档和示例(对于从C#到Haskell的方向)。
最佳答案
我将在这里对FUZxxl的帖子发表评论。
您发布的示例都可以使用FFI
来实现。一旦使用FFI导出函数,就可以将程序编译成DLL。
.NET的设计旨在能够轻松地与C,C++,COM等接口(interface)。这意味着,一旦能够将函数编译为DLL,就可以从.NET(相对)容易地对其进行调用。正如我之前在链接到的其他文章中提到的那样,请记住在导出函数时指定的调用约定。 .NET中的标准是stdcall
,而(大多数)Haskell FFI
导出示例使用ccall
。
到目前为止,我发现FFI可以导出的唯一限制是polymorphic types
或未完全应用的类型。例如除了*
类型以外的任何内容(例如,您不能导出Maybe
,但可以导出Maybe Int
)。
我编写了Hs2lib工具,该工具将自动覆盖并导出示例中具有的任何功能。它还可以选择生成unsafe
C#代码,从而使其几乎“即插即用”。之所以选择不安全的代码,是因为它更易于处理指针,从而使数据结构的编码变得更加容易。
为了完整起见,我将详细介绍该工具如何处理您的示例以及如何计划处理多态类型。
导出高阶函数时,需要稍作更改。高阶参数需要成为FunPtr的元素。基本上,它们被视为显式函数指针(或C#中的委托(delegate)),这就是在命令式语言中通常如何实现更高的有序性。
假设我们将
Int
转换为CInt
,则double类型将从(Int -> Int) -> Int -> Int
进入
FunPtr (CInt -> CInt) -> CInt -> IO CInt
这些类型是为包装函数(在这种情况下为
doubleA
)生成的,而不是double
本身被导出。包装函数在导出的值和原始函数的预期输入值之间映射。需要IO,因为构造FunPtr
并非纯粹的操作。要记住的一件事是,构造或取消引用
FunPtr
的唯一方法是通过静态创建导入来指示GHC为此创建存根。foreign import stdcall "wrapper" mkFunPtr :: (Cint -> CInt) -> IO (FunPtr (CInt -> CInt))
foreign import stdcall "dynamic" dynFunPtr :: FunPtr (CInt -> CInt) -> CInt -> CInt
“包装器” 函数允许我们创建一个
FunPtr
,而“动态” FunPtr
允许我们遵循一个。在C#中,我们将输入声明为
IntPtr
,然后使用Marshaller
帮助函数Marshal.GetDelegateForFunctionPointer创建可以调用的函数指针,或使用反函数从函数指针创建IntPtr
。还请记住,作为参数传递给FunPtr的函数的调用约定必须与传递参数的函数的调用约定相匹配。换句话说,将
&foo
传递给bar
要求foo
和bar
具有相同的调用约定。实际上,导出用户数据类型非常简单。对于每个需要导出的数据类型,必须为此类型创建Storable实例。该实例指定了GHC能够导出/导入此类型所需的编码信息。除其他事项外,您还需要定义类型的
size
和alignment
,以及如何读取/写入指针类型的值。我将Hsc2hs部分用于此任务(因此文件中的C宏)。仅带的
newtypes
或datatypes
很容易。这些成为平面结构,因为构造/销毁这些类型时只有一种可能的选择。具有多个构造函数的类型将成为一个并集(在C#中将Layout
属性设置为Explicit
的结构)。但是,我们还需要包含一个枚举,以标识正在使用的构造。通常,数据类型
Single
定义为data Single = Single { sint :: Int
, schar :: Char
}
创建以下
Storable
实例instance Storable Single where
sizeOf _ = 8
alignment _ = #alignment Single_t
poke ptr (Single a1 a2) = do
a1x <- toNative a1 :: IO CInt
(#poke Single_t, sint) ptr a1x
a2x <- toNative a2 :: IO CWchar
(#poke Single_t, schar) ptr a2x
peek ptr = do
a1' <- (#peek Single_t, sint) ptr :: IO CInt
a2' <- (#peek Single_t, schar) ptr :: IO CWchar
x1 <- fromNative a1' :: IO Int
x2 <- fromNative a2' :: IO Char
return $ Single x1 x2
和C结构
typedef struct Single Single_t;
struct Single {
int sint;
wchar_t schar;
} ;
函数
foo :: Int -> Single
将作为foo :: CInt -> Ptr Single
导出而具有多个构造函数的数据类型
data Multi = Demi { mints :: [Int]
, mstring :: String
}
| Semi { semi :: [Single]
}
生成以下C代码:
enum ListMulti {cMultiDemi, cMultiSemi};
typedef struct Multi Multi_t;
typedef struct Demi Demi_t;
typedef struct Semi Semi_t;
struct Multi {
enum ListMulti tag;
union MultiUnion* elt;
} ;
struct Demi {
int* mints;
int mints_Size;
wchar_t* mstring;
} ;
struct Semi {
Single_t** semi;
int semi_Size;
} ;
union MultiUnion {
struct Demi var_Demi;
struct Semi var_Semi;
} ;
Storable
实例相对来说比较简单,应该从C struct定义更容易理解。我的依赖项跟踪程序将针对
Maybe Int
类型发出对Int
和Maybe
类型的依赖关系。这意味着,当生成Storable
的Maybe Int
实例时,头部看起来像instance Storable Int => Storable (Maybe Int) where
也就是说,只要存在用于应用程序参数的Storable实例,类型本身也可以导出。
由于
Maybe a
被定义为具有多态参数Just a
,因此在创建结构时,会丢失一些类型信息。这些结构将包含void*
参数,您必须手动将其转换为正确的类型。在我看来,另一种选择太麻烦了,那就是还要创建专门的结构。例如。 struct MaybeInt。但是,普通模块可能生成的专用结构数量会以这种方式 swift 爆炸。 (稍后可以将其添加为标志)。为了缓解这种信息丢失,我的工具将导出针对该功能找到的所有
Haddock
文档,作为生成的include中的注释。它还会将原始的Haskell类型签名也放入注释中。然后,IDE会将这些内容作为其Intellisense(代码互补)的一部分进行显示。与所有这些示例一样,我已经省略了.NET方面的代码,如果您对此感兴趣,则可以仅查看的输出Hs2lib 。
还有一些其他类型需要特殊处理。特别是
Lists
和Tuples
。多态类型
e.g. map :: (a -> b) -> [a] -> [b]
的问题是size
和a
的b
未知。也就是说,由于我们不知道它们是什么,因此无法为参数和返回值保留空间。我计划通过允许您为a
和b
指定可能的值并为这些类型创建专门的包装函数来支持此功能。另一方面,在命令式语言中,我将使用overloading
向用户展示您选择的类型。至于类,Haskell的开放世界假设通常是一个问题(例如可以随时添加一个实例)。但是,在编译时,仅提供静态已知的实例列表。我打算提供一个选项,该选项将使用这些列表自动导出尽可能多的专用实例。例如export
(+)
在编译时为所有已知的Num
实例导出一个专用函数(例如Int
,Double
等)。该工具也相当值得信赖。由于我无法真正检查代码的纯度,因此我始终相信程序员是诚实的。例如。您不会将具有副作用的函数传递给需要纯函数的函数。坦白地说,将高阶论点标记为不避免问题是不正确的。
我希望这会有所帮助,并且我希望这不会太久。
更新:我最近发现了一些大陷阱。我们必须记住,.NET中的String类型是不可变的。因此,当编码器将其发送给Haskell代码时,我们得到的CWString就是原始副本。我们有来释放它。在C#中执行GC时,它不会影响CWString,它是一个副本。
但是问题是,当我们在Haskell代码中释放它时,我们不能使用freeCWString。指针未分配C(msvcrt.dll)的分配。有三种方法(我知道)可以解决此问题。
关于c# - 在C#中使用高阶Haskell类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6562378/