在C#中使用高阶Haskell类型 - c#

如何使用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的方向)。

c#大神给出的解决方案

我将在这里对FUZxxl的帖子发表评论。
您发布的示例都可以通过FFI使用。一旦使用FFI导出函数,就可以将程序编译成DLL。

.NET的设计旨在能够轻松地与C,C ++,COM等接口。这意味着一旦能够将函数编译成DLL,就可以从.NET(相对)容易地对其进行调用。正如我在链接到的其他文章中之前提到的那样,请记住在导出函数时指定的调用约定。 .NET中的标准是stdcall,而(大多数)Haskell FFI使用ccall导出的示例。

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

我编写了一个工具Hs2lib,该工具将自动覆盖并导出示例中具有的任何功能。它还可以选择生成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

“包装器”功能允许我们创建一个FunPtr,而“动态” FunPtr允许我们引用一个。

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

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

用户数据类型

实际上,导出用户数据类型非常简单。对于每个需要导出的数据类型,必须为此类型创建一个Storable实例。该实例指定了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定义中应该更容易理解。

应用类型

我的依赖项跟踪程序将为类型Maybe IntInt都发出依赖项。这意味着,为Maybe生成Storable实例时,头部看起来像

instance Storable Int => Storable (Maybe Int) where

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

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

为了缓解这种信息丢失,我的工具将导出针对该功能找到的所有void*文档,作为生成的include中的注释。它还会将原始的Haskell类型签名也放入注释中。然后,IDE会将这些内容作为其Intellisense(代码互补)的一部分进行呈现。

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

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

列表需要传递要进行编组的数组的大小,因为我们正在与不隐式知道数组大小的非托管语言进行交互。相反,当我们返回列表时,我们还需要返回列表的大小。
元组是特殊的内置类型,为了导出它们,我们必须首先将它们映射为“普通”数据类型,然后将其导出。在该工具中,最多完成8个元组。

多态类型

多态类型Tuples的问题是e.g. map :: (a -> b) -> [a] -> [b]sizea未知。也就是说,由于我们不知道它们是什么,因此无法为参数和返回值保留空间。我计划通过允许您为ba指定可能的值并为这些类型创建专门的包装器函数来支持这一点。另一方面,在命令性语言中,我将使用b向用户展示您选择的类型。

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

该工具也相当值得信赖。由于我无法真正检查代码的纯度,因此我始终相信程序员是诚实的。例如。您不会将具有副作用的函数传递给需要纯函数的函数。坦白地说,将高阶论点标记为不合理,以避免出现问题。

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

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

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

调用Haskell函数时,请在C#代码中使用char *而不是String。然后,您可以在调用return时使指针空闲,或者使用fixed初始化函数。
在Haskell中导入CoTaskMemFree并在Haskell中释放指针
使用StringBuilder而不是String。我对此并不完全确定,但是想法是,由于StringBuilder是作为本机指针实现的,因此Marshaller只是将此指针传递给了您的Haskell代码(它也可以同时更新)。调用返回后执行GC时,应释放StringBuilder。

剃刀付款集成->如何通过关闭按钮X检测剃刀付款模型是否关闭 - javascript

当用户关闭而无需付款时,我在CI框架中使用Razorpay,请创建razor支付模型,然后取消订单,我希望按状态更改为已取消的状态触发查询。所以我怎么能检测到这一点。我已经通过单击jQuery单击关闭功能但无法使用... javascript大神给出的解决方案 Razorpay提供了JS方法来检测模式关闭。您编写的任何JS代码都不会在结帐页面上运行,因为它是…

LeetCode题解求一根绳子被切两刀能组成一个三角形的概率。

如题题解:我们可以设绳长为1,设:- 其中两段长为x, y且x, y都>0- 故第三段长为1-x-y且>0故可以在二维坐标轴画出一个三角形(由x=0;y=0;1-x-y=0围成)要想构成三角形还要满足:- x+y > 1-x-y => x+y > 0.5- x+1-x-y > y => y < 0.5- y+1…

LeetCode题解多人运动

已知小猪每晚都要约好几个女生到酒店房间。每个女生 i 与小猪约好的时间由 [si , ei]表示,其中 si 表示女生进入房间的时间, ei 表示女生离开房间的时间。由于小猪心胸开阔,思想开明,不同女生可以同时存在于小猪的房间。请计算出小猪最多同时在做几人的「多人运动」。例子: Input : [[ 0 , 30] ,[ 5 , 10 ] , [15 , 2…

LeetCode题解136.single-number

题目地址 https://leetcode.com/problems/single-number/description/ 题目描述 Given a non-empty array of integers, every element appears twice except for one. Find that single one. Note: Your…

Python numpy数据指针地址无需更改即可更改 - python

编辑经过一些摆弄之后,到目前为止,我已经隔离了以下状态:一维数组在直接输入变量时提供两个不同的地址,而在使用print()时仅提供一个地址2D数组(或矩阵)在直接输入变量时提供三个不同的地址,在使用print()时提供两个地址3D数组在直接输入变量时提供两个不同的地址,而在使用print()时仅给出一个(显然与一维数组相同)像这样:>>> …