在C#中使用高阶Haskell类型

如何使用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#? 我需要知道的最重要的function是传递类型类(如Monad或Arrow)。

我已经知道如何将Haskell库编译为DLL并在C#中使用,但仅适用于一阶函数。 我也知道Stackoverflow – 在.NET中调用Haskell函数 , 为什么GHC不能用于.NET和hs-dotnet ,我没有找到任何文档和样本(用于C#到Haskell方向)。

我将在此详细阐述我对FUZxxlpost的评论。
您发布的示例都可以使用FFI 。 使用FFI导出函数后,您可以将已编程的程序编译为DLL。

.NET的设计旨在能够轻松地与C,C ++,COM等进行交互。这意味着一旦您能够将函数编译为DLL,就可以从.NET(相对)轻松地调用它。 正如我之前在你发布的其他post中提到过的那样,请记住导出函数时指定的调用约定。 .NET中的标准是stdcall ,而(大多数)使用ccall导出Haskell FFI ccall

到目前为止,我发现FFI可以导出的唯一限制是polymorphic types或未完全应用的类型。 例如,除了种类之外的任何东西* (你不能导出Maybe但你可以导出Maybe Int )。

我编写了一个工具Hs2lib ,可以自动覆盖和导出示例中的任何function。 它还可以生成unsafe C#代码,使其非常“即插即用”。 我选择不安全代码的原因是因为它更容易处理指针,这反过来又更容易为数据结构进行编组。

为了完整,我将详细说明该工具如何处理您的示例以及我计划如何处理多态类型。

  • 高阶函数

导出高阶函数时,需要稍微更改函数。 高阶参数需要成为FunPtr的元素。 基本上它们被视为显式函数指针(或c#中的委托),这是通常在命令式语言中完成更高阶的有序性。
假设我们将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 

“包装器”function允许我们创建一个FunPtr ,而“动态” FunPtr允许我们一个人。

在C#中,我们将输入声明为IntPtr ,然后使用Marshaller辅助函数Marshal.GetDelegateForFunctionPointer来创建我们可以调用的函数指针,或者使用反函数从函数指针创建IntPtr

还要记住,作为参数传递给FunPtr的函数的调用约定必须匹配传递参数的函数的调用约定。 换句话说,将&foo传递给bar要求foobar具有相同的调用约定。

  • 用户数据类型

导出用户数据类型实际上非常简单。 对于需要导出的每种数据类型,必须为此类型创建可存储实例。 此实例指定GHC需要的编组信息,以便能够导出/导入此类型。 除此之外,您还需要定义类型的sizealignment ,以及如何读取/写入指针类型的值。 我部分使用Hsc2hs执行此任务(因此文件中的C宏)。

只有一个构造函数的newtypesdatatypes很容易。 这些变成了扁平结构,因为在构造/破坏这些类型时只有一种可能的替代方案。 具有多个构造函数的类型成为联合(在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定义开始。

  • 应用类型

我的依赖关系跟踪器会针对类型IntMaybe的类型发出for Maybe Int for依赖于类型IntMaybe 。 这意味着,当生成Maybe IntStorable实例时,头部看起来像

 instance Storable Int => Storable (Maybe Int) where 

也就是说,只要应用程序的参数有一个Storable实例,也可以导出类型本身。

由于Maybe a被定义为具有多态参数Just a ,因此在创建结构时,某些类型信息会丢失。 结构将包含一个void*参数,您必须手动将其转换为正确的类型。 在我看来,替代方案太麻烦了,那就是创建专门的结构。 例如struct MaybeInt。 但是,可以从正常模块生成的专用结构的数量可以快速地以这种方式爆炸。 (稍后可能会将其添加为标志)。

为了减少这种信息丢失,我的工具会将为该函数找到的任何Haddock文档导出为生成的包含中的注释。 它还会将原始的Haskell类型签名放在注释中。 然后,IDE将这些作为其Intellisense(代码竞争)的一部分呈现。

与所有这些示例一样,我已经省略了.NET方面的代码,如果您对此感兴趣,可以只查看Hs2lib的输出。

还有一些其他需要特殊处理的类型。 特别是ListsTuples

  1. 列表需要传递从中进行编组的数组的大小,因为我们正在与非托管语言接口,其中数组的大小不是隐式已知的。 相反,当我们返回一个列表时,我们还需要返回列表的大小。
  2. 元组是特殊的构建类型,为了导出它们,我们必须首先将它们映射到“普通”数据类型,然后导出它们。 在该工具中,这将完成,直到8元组。

    • 多态类型

多态类型的问题, eg map :: (a -> b) -> [a] -> [b]absize不知道。 也就是说,没有办法为参数保留空间并返回值,因为我们不知道它们是什么。 我计划通过允许您为ab指定可能的值并为这些类型创建专门的包装函数来支持这一点。 在另一个大小,在命令式语言中,我将使用overloading来向用户呈现您选择的类型。

至于类,Haskell的开放世界假设通常是一个问题(例如,可以随时添加实例)。 但是,在编译时,只有一个静态已知的实例列表可用。 我打算提供一个选项,使用这些列表自动导出尽可能多的专用实例。 例如,export (+)在编译时为所有已知的Num实例导出一个专用函数(例如IntDouble等)。

该工具也相当信任。 由于我无法真正检查代码的纯度,我始终相信程序员是诚实的。 例如,您不会将具有副作用的函数传递给需要纯函数的函数。 要诚实,并将更高阶的论点标记为不可避免的问题。

我希望这会有所帮助,我希望这不会太久。

更新 :我最近发现了一些大问题。 我们必须记住,.NET中的String类型是不可变的。 所以当marshaller将它发送给Haskell代码时,我们得到的CWString就是原始的副本。 我们必须释放这个。 在C#中执行GC时,它不会影响CWString,这是一个副本。

但问题是,当我们在Haskell代码中释放它时,我们不能使用freeCWString。 指针未分配C(msvcrt.dll)的alloc。 有三种方法(我知道)可以解决这个问题。

  • 在调用Haskell函数时,在C#代码中使用char *而不是String。 然后,当您调用return时,您可以指向free,或者使用fixed来初始化函数。
  • 在Haskell中导入CoTaskMemFree并释放Haskell中的指针
  • 使用StringBuilder而不是String。 我不完全确定这个,但是我的想法是,因为StringBuilder是作为本机指针实现的,所以Marshaller只是将这个指针传递给你的Haskell代码(也可以通过btw更新它)。 在调用返回后执行GC时,应释放StringBuilder。

您是否尝试过通过FFI导出function? 这允许您为函数创建更多的C-ish接口。 我怀疑可以直接从C#调用Haskell函数。 有关更多信息,请参阅doc。 (上面的链接)。

在做了一些测试之后,我认为通常不可能通过FFI导出带有类型参数的高阶函数和函数。 [需要引证]

好的,感谢FUZxxl,他为“未知类型”提出了一个解决方案。 将数据存储在IO上下文中的Haskell MVar中,并使用一阶函数从C#传递到Haskell。 至少对于简单情况,这可能是解决方案。