Использование типов 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
.
- списки должны получить переданный размер массива, из которого Маршалл, так как мы взаимодействуем с неуправляемыми языками, где размер массивов неявно известен. И наоборот, когда мы возвращаем список, нам также нужно вернуть размер списка.
-
кортежи-это специальная сборка типов, чтобы экспортировать их, мы должны сначала сопоставить их с "нормальным" типом данных и экспортируйте их. В инструменте это делается до 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 с функциями первого порядка. Это может быть решением, по крайней мере, для простых ситуаций.