Usando tipos Haskell de orden superior en C#


Cómo puedo usar y llamar funciones de Haskell con firmas de tipo de orden superior desde C# (DllImport), como...

double :: (Int -> Int) -> Int -> Int -- higher order function

typeClassFunc :: ... -> Maybe Int    -- type classes

data MyData = Foo | Bar              -- user data type
dataFunc :: ... -> MyData

¿Cuál es la firma de tipo correspondiente en C#?

[DllImport ("libHSDLLTest")]
private static extern ??? foo( ??? );

Además (porque puede ser más fácil): ¿Cómo puedo usar tipos Haskell "desconocidos" dentro de C#, para que al menos pueda pasarlos, sin que C# conozca ningún tipo específico? La funcionalidad más importante que necesito saber es pasar una clase de tipo (como Mónada o Flecha).

Ya lo sé cómo compilar una biblioteca Haskell a DLL y usarla dentro de C#, pero solo para funciones de primer orden. También soy consciente de Stackoverflow-Llama a una función Haskell en. NET, ¿Por qué no está GHC disponible para.NET y hs-dotnet, donde no encontré ninguna documentación y muestras (para la dirección de C# a Haskell).

Author: Community, 2011-07-03

3 answers

Elaboraré aquí mi comentario sobre el post de FUZxxl.
Los ejemplos que publicaste son todos posibles usando FFI. Una vez que exportas tus funciones usando FFI puedes como ya has averiguado compilar el programa en una DLL.

. NET fue diseñado con la intención de poder interactuar fácilmente con C, C++, COM, etc. Esto significa que una vez que eres capaz de compilar tus funciones a un DLL, puedes llamarlo (relativamente) fácil desde. NET. Como he mencionado antes en mi otro post a la que hayas vinculado, ten en cuenta la convención de llamada que especifiques al exportar tus funciones. El estándar en. NET es stdcall, mientras que (la mayoría) ejemplos de Haskell FFI exportan usando ccall.

Hasta ahora la única limitación que he encontrado sobre lo que puede ser exportado por FFI es polymorphic types, o tipos que no se aplican completamente. por ejemplo, cualquier cosa que no sea kind * (No se puede exportar Maybe pero se puede exportar Maybe Int por ejemplo).

He escrito una herramienta Hs2lib que cubra y exporte automáticamente cualquiera de las funciones que tiene en su ejemplo. También tiene la opción de generar unsafe código C# que lo hace prácticamente "plug and play". La razón por la que he elegido código inseguro es porque es más fácil manejar punteros con, que a su vez hace que sea más fácil hacer el marshalling para dataestructures.

Para ser completo detallaré cómo la herramienta maneja sus ejemplos y cómo planeo manejar tipos polimórficos.

  • Orden superior funciones

Al exportar funciones de orden superior, la función debe cambiarse ligeramente. Los argumentos de orden superior necesitan convertirse en elementos de FunPtr . Básicamente son tratados como punteros de función explícitos (o delegados en c#), que es cómo se hace típicamente un orden más alto en lenguajes imperativos.
Suponiendo que convertimos Int en CInt el tipo de doble se transforma de

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

Hacia

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

Estos tipos se generan para una función wrapper (doubleA en este caso) que se exporta en lugar de double. Las funciones de envoltura se asignan entre los valores exportados y los valores de entrada esperados para la función original. El IO es necesario porque construir un FunPtr no es una operación pura.
Una cosa a recordar es que la única manera de construir o desreferenciar un FunPtr es mediante la creación estática de importaciones que instruyen a GHC para crear stubs para esto.

foreign import stdcall "wrapper" mkFunPtr  :: (Cint -> CInt) -> IO (FunPtr (CInt -> CInt))
foreign import stdcall "dynamic" dynFunPtr :: FunPtr (CInt -> CInt) -> CInt -> CInt

La función "wrapper" nos permite crear una FunPtr y la "dinámica" FunPtr permite a uno respetar a uno.

En C# declaramos la entrada como IntPtr y luego usamos la función auxiliar Marshaller Marshal.GetDelegateForFunctionPointer para crear un puntero de función que podamos llamar, o la función inversa para crear un IntPtr a partir de un puntero de función.

También recuerde que la convención de llamada de la función que se pasa como argumento al FunPtr debe coincidir con la llamada convención de la función a la que se pasa el argumento. En otras palabras, pasar &foo a bar requiere que foo y bar tengan la misma convención de llamada.

  • Tipos de datos de usuario

Exportar un tipo de datos de usuario es en realidad bastante sencillo. Para cada tipo de datos que necesita ser exportado se debe crear una instancia Storable para este tipo. En estos casos se especifica la información de clasificación que GHC necesita para poder exportar / importar este tipo. Entre otras cosas, necesitará definir el size y alignment del tipo, junto con cómo leer/escribir en un puntero los valores del tipo. Utilizo parcialmente Hsc2hs para esta tarea (de ahí las macros C en el archivo).

newtypes o datatypes con solo un constructor es fácil. Estos se convierten en una estructura plana ya que solo hay una alternativa posible al construir / destruir estos tipos. Los tipos con múltiples constructores se convierten en una unión (a struct with Layout attribute set to Explicit in C#). Sin embargo, también necesitamos incluir una enumeración para identificar qué construcción se está utilizando.

En general, el tipo de datos Single definido como

data Single = Single  { sint   ::  Int
                      , schar  ::  Char
                      }

Crea la siguiente instancia 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

Y la estructura C

typedef struct Single Single_t;

struct Single {
     int sint;
     wchar_t schar;
} ;

La función foo :: Int -> Single se exportaría como foo :: CInt -> Ptr Single While a datatype with multiple constructor

data Multi  = Demi  {  mints    ::  [Int]
                    ,  mstring  ::  String
                    }
            | Semi  {  semi :: [Single]
                    }

Genera el siguiente código 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;
} ;

La instancia Storable es relativamente sencillo y debería seguir más fácilmente de la definición de la estructura C.

  • Tipos aplicados

Mi trazador de dependencias emitiría para para el tipo Maybe Int la dependencia tanto en el tipo Int y Maybe. Esto significa que al generar la instancia Storable para Maybe Int la cabeza se ve como

instance Storable Int => Storable (Maybe Int) where

Es decir, mientras haya una instancia almacenable para los argumentos de la aplicación, el tipo en sí también se puede exportar.

Desde Maybe a se define como tener un argumento polimórfico Just a, al crear las estructuras, se pierde cierta información de tipo. Las estructuras contendrían un argumento void*, que tienes que convertir manualmente al tipo correcto. La alternativa era demasiado engorrosa en mi opinión, que era crear también estructuras especializadas. Por ejemplo, struct MaybeInt. Pero la cantidad de estructuras especializadas que podrían generarse a partir de un módulo normal puede explotar rápidamente de esta manera. (podría agregar esto como una bandera más adelante).

Para aliviar esta pérdida de información mi herramienta exportará cualquier documentación Haddock encontrada para la función como comentarios en los includes generados. También colocará la firma de tipo Haskell original en el comentario. Un IDE entonces presentaría estos como parte de su Intellisense (código compeletion).

Al igual que con todos estos ejemplos he ommited el código para el lado. NET de las cosas, si usted está interesado en que solo puede ver la salida de Hs2lib.

Hay algunos otros tipos que necesitan un tratamiento especial. En particular Lists y Tuples.

  1. Las listas necesitan pasar el tamaño de la matriz desde la que marshall, ya que estamos interactuando con lenguajes no administrados donde el tamaño de las matrices no se conoce implícitamente. Conversly cuando devolvemos una lista, también necesitamos devolver el tamaño de la lista.
  2. Las tuplas son tipos de compilación especiales, para exportarlas, primero hay que asignarlos a un tipo de datos" normal " y exportarlos. En la herramienta esto se hace hasta 8 tuplas.

    • Tipos polimórficos

El problema con los tipos polimórficos e.g. map :: (a -> b) -> [a] -> [b] es que los size de a y b no se conocen. Es decir, no hay forma de reservar espacio para los argumentos y devolver valor ya que no sabemos cuáles son. Planeo apoyar esto permitiéndole especificar posibles valores para a y b y crear función de envoltura especializada para estos tipos. En el otro tamaño, en el lenguaje imperativo usaría overloading para presentar los tipos que ha elegido al usuario.

En cuanto a las clases, la suposición de mundo abierto de Haskell suele ser un problema (por ejemplo, una instancia se puede agregar en cualquier momento). Sin embargo, en el momento de la compilación solo está disponible una lista estáticamente conocida de instancias. Tengo la intención de ofrecer una opción que exportaría automáticamente tantas instancias especializadas como sea posible utilizando esta lista. e. g. export (+) exporta una función especializada para todas las instancias Num conocidas en tiempo de compilación (p.ej. Int, Double, etc).

La herramienta también es bastante confiada. Dado que realmente no puedo inspeccionar la pureza del código, siempre confío en que el programador sea honesto. Por ejemplo, no pasa una función que tiene efectos secundarios a una función que espera una función pura. Sea honesto y marque el argumento de orden superior como impuro para evitar problemas.

Espero que esto ayude, y espero que esto no fue demasiado tiempo.

Actualizar : Hay algo de un gran gotcha que he descubierto recientemente. Tenemos que recordar que el tipo de cadena en. NET es inmutable. Así que cuando el marshaller lo envía al código Haskell, la CWString que obtenemos allí es una copia del original. Nosotros tenemos para liberar esto. Cuando GC se realiza en C# no afectará la CWString, que es una copia.

El problema sin embargo es que cuando lo liberamos en el código Haskell no podemos usar freeCWString. El pointer no fue asignado con C (msvcrt.dll) ' s alloc. Hay tres maneras (que yo sepa) de resolver esto.

  • use char* en su código C# en lugar de String cuando llame a una función Haskell. Luego tiene el puntero a free cuando llama returns, o inicializa la función usando fixed.
  • import CoTaskMemFree en Haskell y liberar el puntero en Haskell
  • use StringBuilder en lugar de String. No estoy del todo seguro acerca de este, pero el la idea es que dado que StringBuilder se implementa como un puntero nativo, el Marshaller simplemente pasa este puntero a su código Haskell (que también puede actualizarlo por cierto). Cuando GC se realiza después de que la llamada regresa, el StringBuilder debe ser liberado.
 18
Author: Phyx,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-08-11 05:10:28

¿Ha intentado exportar las funciones a través del FFI? Esto le permite crear una interfaz más C-ish a las funciones. Dudo que sea posible llamar a las funciones de Haskell directamente desde C#. Consulte el documento para obtener más información. (Enlace arriba).

Después de hacer algunas pruebas, creo que generalmente, no es posible exportar funciones de alto orden y funciones con parámetros de tipo a través de la FFI.[Cita requerida]

 4
Author: fuz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-03 13:44:31

Bien, gracias a FUZxxl, una solución que se le ocurrió para "tipos desconocidos". Almacene los datos en un Haskell MVar dentro del contexto IO y comunique desde C# a Haskell con funciones de primer orden. Esto puede ser la solución al menos para situaciones simples.

 3
Author: Gerold Meisinger,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-03 14:42:48