Использование типов Haskell более высокого порядка в C#

Как я могу использовать и вызывать функции Haskell с сигнатурами типа более высокого порядка из C# (DLLImport), например...

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( ??? );

дополнительно (потому что это может быть проще): как я могу использовать "неизвестные" типы Haskell в C#, чтобы я мог по крайней мере передать их, не зная C# какой-либо конкретный тип? Самая важная функциональность, которую мне нужно знать, - это передать класс типа (например, Monad или Стрелка.)

Я уже знаю как скомпилировать библиотеку Haskell в DLL и использовать в C#, но только для первого порядка функции. Я также знаю о Stackoverflow-вызов функции Haskell в .NET,почему GHC не доступен для .NET и УГ-dotnet ограничителя, где я не нашел никакой документации и образцов (для направления C# в Haskell).

3 ответов


я подробно остановлюсь здесь на своем комментарии к сообщению FUZxxl.
Примеры, которые вы разместили, можно использовать FFI. Как только вы экспортируете свои функции с помощью FFI, вы можете, как вы уже поняли, скомпилировать программу в DLL.

.NET был разработан с целью иметь возможность легко взаимодействовать с C, C++, COM и т.д. Это означает, что как только вы сможете скомпилировать свои функции в DLL, вы можете вызвать его (относительно) легко .Сеть. Как я уже упоминал ранее в мой другой пост, с которым вы связались, имейте в виду, какое соглашение о вызовах вы указываете при экспорте своих функций. Стандарт .Net-это stdcall, в то время как (большинство) примеров Haskell FFI экспортировать с помощью ccall.

до сих пор единственное ограничение, которое я нашел на то, что может быть экспортировано FFI, это polymorphic types, или типы, которые не в полной мере. например, ничего кроме вида * (вы не можете экспортировать Maybe но вы можете экспортировать Maybe Int например).

я написал инструмент Hs2lib это будет охватывать и автоматически экспортировать любую из функций, которые у вас есть в вашем примере. Он также имеет возможность генерации unsafe код C#, который делает его в значительной степени"подключи и играй". Причина, по которой я выбрал небезопасный код, заключается в том, что с ним легче обрабатывать указатели, что, в свою очередь, упрощает маршалинг для datastructures.

чтобы быть полным, я подробно расскажу, как инструмент обрабатывает ваши примеры и как я планирую обрабатывать полиморфные типы.

  • функции высшего порядка!--81-->

при экспорте функций более высокого порядка функция должна быть слегка изменена. Аргументы более высокого порядка должны стать элементами FunPtr. В основном они рассматриваются как явные указатели функций (или делегаты в c#), что обычно делается на императивных языках.
Предполагая, что мы преобразуем Int на CInt тип двойника трансформировался из

(Int -> Int) -> Int -> Int

на

FunPtr (CInt -> CInt) -> CInt -> IO CInt

эти типы генерируются для функции-оболочки (doubleA в этом случае), который экспортируется вместо . Функции-оболочки сопоставляются между экспортированными значениями и ожидаемыми входными значениями исходной функции. IO необходим, потому что построение FunPtr не является чистой операцией.
Следует помнить, что единственный способ построить или разыменовать a FunPtr is by статически создавая импорт, который инструктирует 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 вспомогательную функцию Маршал.GetDelegateForFunctionPointer для создания указателя функции, который мы можем вызвать, или обратной функции для создания IntPtr из указателя на функцию.

также помните, что соглашение о вызове функции, передаваемой в качестве аргумента FunPtr, должно соответствовать соглашению о вызове функции, которой передается аргумент. Другими словами, проходя &foo to bar требует foo и bar иметь такое же вызывающее соглашение.

  • пользовательские типы данных

экспорт данных пользователей на самом деле довольно прямо вперед. Для каждого типа данных, который необходимо экспортировать Storable экземпляр должен быть создан для этого типа. В этом экземпляре указывается информация о маршаллинге, необходимая GHC для экспорта/импорта этого типа. Среди прочего, вам нужно будет определить size и alignment типа, а также Как читать / записывать в указатель значения типа. Я частично использую Hsc2hs для этой задачи (отсюда макросы C в папка.)

newtypes или datatypes С один конструктор легко. Они становятся плоской структурой, так как существует только одна возможная альтернатива при построении/уничтожении этих типов. Типы с несколькими конструкторами становятся объединением (структура с значение Explicit в C#). Однако нам также нужно включить перечисление, чтобы определить, какая конструкция используется.

в общем, типа Single определенными as

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.

  • прикладные виды

мой трассировщик зависимостей будет для emit for для типа Maybe Int в зависимости от типа Int и Maybe. Это означает, что при создании Storable экземпляр Maybe Int голова выглядит как

instance Storable Int => Storable (Maybe Int) where

то есть, пока есть экземпляр Storable для Аргументов приложения, сам тип также может быть экспортирован.

с Maybe a определяется как имеющий полиморфный аргумент Just a при создании структуры, некоторая информация типа потеряна. Структуры будут содержать void* аргумент, который вы должны вручную преобразовать в нужный тип. На мой взгляд, альтернатива была слишком громоздкой, а именно создание специализированных структур. Е. Г. структура MaybeInt. Но количество специализированных структур, которые могут быть созданы из обычного модуля, может быстро взорваться таким образом. (может добавить это как флаг позже).

чтобы облегчить эту потерю информации мой инструмент будет экспортировать Haddock документация, найденная для функции в качестве комментариев в сгенерированном включает. Он также разместит оригинальную подпись типа Haskell в комментарии. Затем IDE представит их как часть своего Intellisense (code compeletion).

как и во всех этих примерах, я использовал код для .NET-стороны вещей, Если вам интересно, что вы можете просто просмотреть вывод Hs2lib.

есть несколько других типов это требует особого отношения. В частности Lists и Tuples.

  1. списки должны получить переданный размер массива, из которого Маршалл, так как мы взаимодействуем с неуправляемыми языками, где размер массивов неявно известен. И наоборот, когда мы возвращаем список, нам также нужно вернуть размер списка.
  2. кортежи-это специальная сборка типов, чтобы экспортировать их, мы должны сначала сопоставить их с "нормальным" типом данных и экспортируйте их. В инструменте это делается до 8-ки.

    • полиморфных типов

проблема с полиморфными типами e.g. map :: (a -> b) -> [a] -> [b] это size of a и b не знаю. То есть, нет способа зарезервировать место для Аргументов и возвращаемого значения, поскольку мы не знаем, что они такое. Я планирую поддержать это, разрешив вам указать возможные значения для a и b и создайте специализированную оболочку функция для этих типов. С другой стороны, на императивном языке я бы использовал overloading чтобы представить выбранные типы пользователю.

что касается классов, предположение открытого мира Haskell обычно является проблемой (например, экземпляр может быть добавлен в любое время). Однако во время компиляции доступен только статически известный список экземпляров. Я намерен предложить вариант, который автоматически экспортирует как можно больше специализированных экземпляров, используя этот список. например, экспорт (+) экспортирует специализированную функцию для всех известных Num экземпляров во время компиляции (например,Int, Double и т. д.).

инструмент также довольно доверчив. Поскольку я не могу проверить код на чистоту, я всегда верю, что программист честен. Е. Г. мы не передаем функцию, которая не имеет побочных эффектов на функцию, которая ожидает, что чистая функция. Будьте честны и отметьте аргумент более высокого порядка как нечистый, чтобы избежать проблем.

надеюсь, это поможет, и я надеюсь, это было не слишком долго.

обновление: есть что-то большое, что я недавно обнаружил. Мы должны помнить, что строковый тип в .NET является неизменяемым. Поэтому, когда маршаллер отправляет его код Хаскелл, CWString мы вам там копию оригинала. Мы!--94-->есть чтобы бесплатно этот. Когда GC выполняется в C# , это не повлияет на CWString, который является копией.

проблема, однако, в том, что когда мы освобождаем его в Код Haskell мы не можем использовать freeCWString. Указатель не был выделен с помощью C (msvcrt.dll файлы) с к alloc. Есть три способа (о которых я знаю) решить эту проблему.

  • используйте char* в коде C# вместо String при вызове функции Haskell. Затем у вас есть указатель на free при вызове returns или инициализации функции с помощью основные.
  • импорт CoTaskMemFree в Haskell и освободить указатель в Haskell
  • использовать StringBuilder вместо String. Я не совсем уверен в этом, но идея в том, что, поскольку StringBuilder реализован как собственный указатель, Маршаллер просто передает этот указатель на ваш код Haskell (который также может обновить его кстати). Когда GC выполняется после возврата вызова, StringBuilder должен быть освобожден.

вы пытались экспортировать функции через FFI? Это позволяет создать более C-ish интерфейс для функций. Я сомневаюсь, что можно вызвать функции Haskell непосредственно из C#. Дополнительные сведения см. В документе doc. (Ссылка выше).

после выполнения некоторых тестов я думаю, что, как правило, невозможно экспортировать функции высокого порядка и функции с параметрами типа через FFI.[править]


хорошо, благодаря FUZxxl, решение, которое он придумал для "неизвестных типов". Храните данные в Haskell MVar в контексте ввода-вывода и общаться с C# в Haskell с функциями первого порядка. Это может быть решением, по крайней мере, для простых ситуаций.