¿Es posible tener una función que toma una llamada a una función extraña donde algunos de los argumentos de la función ajena son CString y devuelve una función que acepta String en su lugar?
¿Es posible, usted pregunta?
<lambdabot> The answer is: Yes! Haskell can do that.
Ok. Lo bueno es que lo aclaramos.
calentamiento con unos trámites tediosos:
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
Ah, que no es tan mala. Mira, mamá, ¡no hay superposiciones!
El problema parece encajar en las funciones IO, ya que todo lo que se convierte en CStrings como newCString o withCString son IO.
Derecha. Lo que hay que observar aquí es que hay dos asuntos interrelacionados con los que preocuparnos: una correspondencia entre dos tipos, que permite conversiones; y cualquier contexto adicional introducido al realizar una conversión. Para lidiar con esto por completo, haremos que ambas partes sean explícitas y las mezclaremos de forma adecuada. También tenemos que prestar atención a varianza; levantar una función completa requiere trabajar con tipos tanto en posición covariante como contravariante, por lo que necesitaremos conversiones en ambas direcciones.
Ahora, dada una función que deseamos traducir, el plan es algo como esto:
- Convierte el argumento de la función, recibiendo un nuevo tipo y un poco de contexto.
- Deferir el contexto al resultado de la función, para obtener el argumento como lo queremos.
- Collapse contextos redundantes cuando sea posible
- recursiva traducen resultado de la función, para hacer frente a las funciones múltiples de argumentos
Bueno, eso no suena demasiado difícil. En primer lugar, los contextos explícitos:
class (Functor f, Cxt t ~ f) => Context (f :: * -> *) t where
type Collapse t :: *
type Cxt t :: * -> *
collapse :: t -> Collapse t
Este dice que tenemos un contexto f
, y algún tipo t
con ese contexto. La función de tipo Cxt
extrae el contexto simple de t
y Collapse
intenta combinar contextos si es posible. La función collapse
nos permite usar el resultado de la función tipo.
Por ahora, tenemos contextos puros y IO
:
newtype PureCxt a = PureCxt { unwrapPure :: a }
instance Context IO (IO (PureCxt a)) where
type Collapse (IO (PureCxt a)) = IO a
type Cxt (IO (PureCxt a)) = IO
collapse = fmap unwrapPure
{- more instances here... -}
bastante simple. Manejar varias combinaciones de contextos es un poco tedioso, pero las instancias son obvias y fáciles de escribir.
También necesitaremos una forma de determinar el contexto dado un tipo para convertir. Actualmente, el contexto es el mismo yendo en cualquier dirección, pero ciertamente es concebible que sea de otra manera, así que los he tratado por separado. Por lo tanto, tenemos dos familias tipo, suministrando el nuevo contexto más externa para una conversión de importación/exportación:
type family ExpCxt int :: * -> *
type family ImpCxt ext :: * -> *
Algunos casos de ejemplo:
type instance ExpCxt() = PureCxt
type instance ImpCxt() = PureCxt
type instance ExpCxt String = IO
type instance ImpCxt CString = IO
El siguiente, la conversión de tipos individuales. Nos preocuparemos por la recursión más tarde.Es hora de otra clase de tipo:
class (Foreign int ~ ext, Native ext ~ int) => Convert ext int where
type Foreign int :: *
type Native ext :: *
toForeign :: int -> ExpCxt int ext
toNative :: ext -> ImpCxt ext int
Esto dice que dos tipos ext
y int
son únicamente convertibles entre sí. Me doy cuenta de que podría no ser deseable tener siempre solo un mapeo para cada tipo, pero no tenía ganas de complicar más las cosas (al menos, no ahora).
Como se mencionó, también pospuse el manejo de conversiones recursivas aquí; probablemente podrían combinarse, pero sentí que sería más claro de esta manera. Las conversiones no recursivas tienen asignaciones simples y bien definidas que introducen un contexto correspondiente, mientras que las conversiones recursivas necesitan propagarse y fusionar contextos y ocuparse de distinguir los pasos recursivos del caso base.
Ah, y es posible que ya hayas notado el divertido negocio de tigres contoneantes en los contextos de clase. Eso indica una restricción de que los dos tipos deben ser iguales; en este caso, relaciona cada función de tipo con el parámetro de tipo opuesto, que da la naturaleza bidireccional mencionada anteriormente. Er, es probable que quieras tener un GHC bastante reciente. En los GHC más antiguos, esto necesitaría dependencias funcionales en su lugar, y se escribiría como algo como class Convert ext int | ext -> int, int -> ext
.
Las funciones de conversión de nivel de término son bastante simples: tenga en cuenta la aplicación de función de tipo en su resultado; la aplicación se asocia de izquierda como siempre, de modo que eso solo aplica el contexto de las familias de tipos anteriores. También tenga en cuenta el cruce en los nombres, en que el exportación contexto proviene de una búsqueda utilizando el tipo nativo.
Por lo tanto, podemos convertir los tipos que no necesitan IO
:
instance Convert CDouble Double where
type Foreign Double = CDouble
type Native CDouble = Double
toForeign = pure . realToFrac
toNative = pure . realToFrac
... así como los tipos que hacen:
instance Convert CString String where
type Foreign String = CString
type Native CString = String
toForeign = newCString
toNative = peekCString
ahora a atacar el corazón de la materia y traducir funciones completas recursivamente. No debería sorprender que haya introducido otra clase de tipo. En realidad, dos, ya que he separado las conversiones de importación/exportación esta vez.
class FFImport ext where
type Import ext :: *
ffImport :: ext -> Import ext
class FFExport int where
type Export int :: *
ffExport :: int -> Export int
Nada interesante aquí. A estas alturas, puede estar notando un patrón común: estamos haciendo cantidades aproximadamente iguales de computación tanto a nivel de término como de tipo, y las estamos haciendo en tándem, incluso hasta el punto de imitar nombres y estructura de expresiones. Esto es bastante común si está haciendo un cálculo de tipo de letra para cosas que involucran valores reales, ya que GHC se pone quisquilloso si no entiende lo que está haciendo. Alinear las cosas de esta manera reduce los dolores de cabeza significativamente.
De todos modos, para cada una de estas clases, necesitamos una instancia para cada caso base posible, y una para el caso recursivo. Por desgracia, no podemos tener fácilmente un caso base genérico, debido a las habituales tonterías molestas con la superposición. Podría hacerse usando fundeps y escribir condicionales de igualdad, pero ... ugh. Quizas mas tarde. Otra opción sería parametrizar la función de conversión mediante un número de nivel de tipo que proporcione la profundidad de conversión deseada, que tiene la desventaja de ser menos automática, pero también se beneficia al ser explícita, como la menor probabilidad de tropezar con polimorfismo o tipos ambiguos.
Por ahora, voy a asumir que cada función termina con algo en IO
, ya que es distinguible de IO a
a -> b
sin solapamiento.
En primer lugar, el caso base:
instance (Context IO (IO (ImpCxt a (Native a)))
, Convert a (Native a)
) => FFImport (IO a) where
type Import (IO a) = Collapse (IO (ImpCxt a (Native a)))
ffImport x = collapse $ toNative <$> x
Las limitaciones aquí valer un contexto específico utilizando un caso conocido, y que tenemos algún tipo de base con una conversión. De nuevo, observe la estructura paralela compartida por la función de tipo Import
y la función de término ffImport
. La idea real aquí debería ser bastante obvia: asignamos la función de conversión sobre IO
, creando un contexto anidado de algún tipo, luego usamos Collapse
/collapse
para limpiarlo después.
El caso recursivo es similar, pero más elaborado:
instance (FFImport b, Convert a (Native a)
, Context (ExpCxt (Native a)) (ExpCxt (Native a) (Import b))
) => FFImport (a -> b) where
type Import (a -> b) = Native a -> Collapse (ExpCxt (Native a) (Import b))
ffImport f x = collapse $ ffImport . f <$> toForeign x
Hemos añadido una restricción FFImport
para la llamada recursiva, y la disputa contexto se ha vuelto más incómodo porque no sabemos exactamente lo que se es, simplemente, especificar lo suficiente para asegurarnos de que podamos manejarlo. Tenga en cuenta también la contradicción aquí, en el sentido de que estamos convirtiendo la función en tipos nativos, pero convirtiendo el argumento en un tipo foráneo. Aparte de eso, sigue siendo bastante simple.
Ahora, he omitido algunas instancias en este punto, pero todo lo demás sigue los mismos patrones que el anterior, así que saltemos hasta el final y analicemos los productos. Algunas funciones imaginarias extranjeros:
foreign_1 :: (CDouble -> CString -> CString -> IO())
foreign_1 = undefined
foreign_2 :: (CDouble -> SizedArray a -> IO CString)
foreign_2 = undefined
y conversiones:
imported1 = ffImport foreign_1
imported2 = ffImport foreign_2
Qué, no hay firmas de tipos? ¿Funcionó?
> :t imported1
imported1 :: Double -> String -> [Char] -> IO()
> :t imported2
imported2 :: Foreign.Storable.Storable a => Double -> AsArray a -> IO [Char]
Sí, esa es la inferido tipo. Ah, eso es lo que me gusta ver.
Editar: Para cualquier persona que quiera probar esto, yo he tomado el código completo para la demostración aquí, lo limpió un poco, y uploaded it to github.
nos muestras lo que has escrito? –
Este es un trabajo bastante complicado :-) Imagino que la respuesta, si existe, es demasiado dolorosa para un uso real. ¿Has mirado 'hsc2hs'? Es bastante potente y puede generar el tipo de firmas que desee como paso de preproceso. – sclv
Una solución que he estado considerando es hacer algo así como una función convertNth, que tomaría un número y una función, y haría la conversión a esa posición. Creo que entiendo cómo funcionaría eso, aunque aún no lo he probado, así que tal vez presente alguna dificultad en la que no haya pensado. El lado positivo es que aún podría usar mi función existente para cadenas y solo tendré que llamar explícitamente cadenas. Idealmente, por supuesto, yo u otra persona simplemente descubriría cómo manejar automáticamente las cadenas. – ricree