ventajas programacion orientada objetos librerias inteligencia imprimir importar funcional espaƱol ejemplos desventajas como artificial c# haskell ffi

c# - orientada - programacion funcional ventajas y desventajas



Usando tipos de Haskell de orden superior en C# (3)

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

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

¿Cómo puedo usar y llamar a las funciones de Haskell con firmas de tipo de orden superior de 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áles son las firmas de tipo correspondientes en C #?

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

Además (porque puede ser más fácil): ¿Cómo puedo usar tipos de Haskell "desconocidos" dentro de C #, por lo que al menos puedo pasarlos, sin que C # sepa 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 sé cómo compilar una biblioteca Haskell en DLL y usarla dentro de C #, pero solo para las funciones de primer orden. También estoy al tanto de Stackoverflow: llame a una función de Haskell en .NET , ¿por qué no está disponible GHC para .NET y hs-dotnet , donde no encontré NINGUNA documentación y muestras (para el C # a la dirección de Haskell)?


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


Voy a elaborar aquí en mi comentario sobre la publicación de FUZxxl.
Los ejemplos que publicaste son todos posibles usando FFI . Una vez que haya exportado sus funciones usando FFI, puede, como ya ha descubierto, 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 pueda compilar sus funciones en una DLL, puede llamarlo (relativamente) fácil desde .NET. Como mencioné anteriormente en mi otra publicación a la que te has vinculado, ten en cuenta qué convención de llamadas especificas al exportar tus funciones. El estándar en .NET es stdcall , mientras que (la mayoría) de los ejemplos de exportación de Haskell FFI mediante ccall .

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

He escrito una herramienta Hs2lib que cubriría y exportaría automáticamente cualquiera de las funciones que tiene en su ejemplo. También tiene la opción de generar unsafe código C # unsafe que lo hace prácticamente "plug and play". La razón por la que elegí el código no seguro es porque es más fácil manejar los punteros, lo que a su vez hace que sea más fácil realizar el cálculo de las estructuras de datos.

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

  • Funciones de orden superior

Al exportar funciones de orden superior, la función debe cambiarse ligeramente. Los argumentos de orden superior deben convertirse en elementos de FunPtr . Básicamente, se tratan como punteros de función explícita (o delegados en c #), que es la forma en que se realiza una mayor ordenación en los idiomas imperativos.
Asumiendo que convertimos Int en CInt el tipo de doble se transforma de

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

dentro

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

Estos tipos se generan para una función de envoltura ( 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 para recordar es que la única forma de construir o desreferenciar un FunPtr es mediante la creación estática de importaciones que le indiquen a GHC que cree talones.

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 un FunPtr y la "dinámica" FunPtr permite deferencia uno.

En C # declaramos la entrada como un IntPtr y luego usamos la función auxiliar Marshaller para crear un puntero de función al que podemos llamar, o la función inversa para crear un IntPtr desde un puntero de función.

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

  • Tipos de datos del usuario

Exportar un tipo de datos de usuario es bastante sencillo. Para cada tipo de datos que deba exportarse, se debe crear una instancia Storable para este tipo. Estas instancias especifican la información de clasificación que GHC necesita para poder exportar / importar este tipo. Entre otras cosas, tendría que definir el size y la alignment del tipo, junto con la forma de leer / escribir en un puntero los valores del tipo. Uso parcialmente Hsc2hs para esta tarea (de ahí las macros C en el archivo).

newtypes o datatypes con un solo 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 (una estructura con el atributo Layout establecido en Explicit en C #). Sin embargo, también debemos incluir una enumeración para identificar qué construcción se está utilizando.

en general, el tipo Single 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 mientras que un tipo de datos con múltiples constructores

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 sencilla y debería seguirse más fácilmente desde la definición de la estructura C.

  • Tipos aplicados

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

instance Storable Int => Storable (Maybe Int) where

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

Dado que 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 debe convertir manualmente al tipo correcto. La alternativa era demasiado engorrosa en mi opinión, que era crear también estructuras especializadas. Ej. 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 facilitar esta pérdida de información, mi herramienta exportará cualquier documentación de Haddock encontrada para la función como comentarios en los incluidos generados. También colocará la firma original del tipo Haskell en el comentario también. Un IDE luego los presentaría 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 está interesado en eso, puede ver la salida de Hs2lib .

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

  1. Las listas deben pasar el tamaño de la matriz desde la cual se va a realizar la recopilación, ya que estamos interactuando con idiomas no administrados en los que el tamaño de las matrices no se conoce de forma implícita. A la inversa, cuando devolvemos una lista, también debemos devolver el tamaño de la lista.
  2. Las tuplas son tipos especiales de compilación. Para exportarlos, primero debemos asignarlos a un tipo de datos "normal" y exportarlos. En la herramienta esto se hace hasta 8-tuplas.

    • Tipos polimorfos

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

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

La herramienta también es bastante confiada. Como realmente no puedo inspeccionar el código en busca de pureza, 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 no haya sido demasiado largo.

Actualización : hay algo así como un gran golpe que he descubierto recientemente. Debemos recordar que el tipo String en .NET es inmutable. Entonces, cuando el oficial de reparto lo envía a nuestro código Haskell, el CWString que obtenemos es una copia del original. Tenemos que liberar esto. Cuando GC se realiza en C #, no afectará a la CWString, que es una copia.

Sin embargo, el problema es que cuando lo liberamos en el código de Haskell no podemos usar freeCWString. El puntero no se asignó con la asignación de C (msvcrt.dll). Hay tres formas (que conozco) para resolver esto.

  • use char * en su código C # en lugar de String cuando llame a una función de Haskell. Luego tiene el puntero para liberar cuando llama a devoluciones, o inicializar la función usando fixed .
  • importar CoTaskMemFree en Haskell y liberar el puntero en Haskell
  • use StringBuilder en lugar de String. No estoy completamente seguro de esto, pero 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 se realiza GC después de que se devuelve la llamada, StringBuilder debe liberarse.