Datos altos

Sí, sí, no soñaste y escuchaste bien: es de un tipo muy elevado. Tipo es un término de teoría de tipo que esencialmente significa un tipo de tipo [datos].

Pero primero, algunas letras.

Se publicaron varios artículos sobre Habré, que describían en detalle el método de validación de datos en lenguajes funcionales.

Este artículo son mis cinco centavos en este bombo publicitario. Consideraremos la validación de datos en Haskell.

Validación de tipo


Anteriormente se consideró un ejemplo de una técnica de validación que utiliza la validación de tipo:

type EmailContactInfo = String type PostalContactInfo = String data ContactInfo = EmailOnly EmailContactInfo | PostOnly PostalContactInfo | EmailAndPost (EmailContactInfo, PostalContactInfo) data Person = Person { pName :: String, , pContactInfo :: ContactInfo, } 

Con este método, es simplemente imposible crear datos incorrectos. Sin embargo, a pesar de que dicha validación es muy sencilla de crear y leer, su uso te obliga a escribir mucha rutina y hacer muchos cambios en el código. Esto significa que el uso de dicho método está limitado solo para datos realmente importantes.

Alta validación de datos




En este artículo, veremos un método de validación diferente: el uso de datos de alta calidad.

Supongamos que tenemos un tipo de datos:

 data Person = Person { pName :: String , pAge :: Int } 

Y validaremos los datos solo si todos los campos del registro son válidos.
Dado que Haskell es superior en funcionalidad a la mayoría de los lenguajes funcionales, puede deshacerse fácilmente de la mayoría de las rutinas.

Aquí es posible y, por lo tanto, este método es ampliamente utilizado entre los autores de bibliotecas en Haskell.

Para fines de discusión, imaginemos que queremos que un usuario complete información personal a través de un formulario web u otra cosa. En otras palabras, es posible que puedan estropear el llenado de alguna parte de la información, no necesariamente invalidando el resto de la estructura de datos. Si llenaron con éxito toda la estructura, nos gustaría obtener un pincel de Persona lleno.

Una forma de modelar es usar un segundo tipo de datos:

 data MaybePerson = MaybePerson  { mpName :: Maybe String  , mpAge :: Maybe Int  } 

donde, recuerdo que se usa el tipo opcional:

 -- already in Prelude data Maybe a = Nothing | Just a 

A partir de aquí, la función de validación es bastante simple:

 validate :: MaybePerson -> Maybe Person validate (MaybePerson name age) =  Person <$> name <*> age 

Un poco más de detalle sobre las funciones (<$>) y (<*>)
La función (<$>) es solo un sinónimo infijo para fmap Functor

 -- already in Prelude fmap :: Functor f => (a -> b) -> fa -> fb (<$>) :: Functor f => (a -> b) -> fa -> fb (<$>) = fmap 

AND (<*>) es una función de aplicar el Functor Aplicativo

 -- already in Prelude (<*>) :: Applicative f => f (a -> b) -> fa -> fb 

Y para un tipo opcional, estas funciones tienen la siguiente definición

 -- already in Prelude (<$>) :: (a -> b) -> Maybe a -> Maybe b _ <$> Nothing = Nothing f <$> (Just a) = Just (fa) (<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b (Just f) <*> m = f <$> m Nothing <*> _ = Nothing 


Nuestra validación funciona, pero es molesto escribir código de rutina adicional a mano, ya que esto se hace de forma completamente mecánica. Además, la duplicación de estos esfuerzos significa que necesitaremos usar nuestros cerebros en el futuro para asegurarnos de que las tres definiciones permanezcan sincronizadas. ¿Sería genial si el compilador pudiera manejar esto?

SORPRESA! ¡PUEDE! ¡Una familia alta nos ayudará!

En Haskell, existe un género, es un tipo , y la explicación más simple y precisa es que un género es un tipo de tipo [datos]. El género más utilizado es * , que se puede llamar "final"

 ghci> :k Int Int :: * ghci> :k String String :: * ghci> :k Maybe Int Maybe Int :: * ghci> :k Maybe String Maybe String :: * ghci> :k [Int] [Int] :: * 

¿Y qué tipo de tal vez ?
 ghci> :k Maybe Maybe :: * -> * ghci> :k [] [] :: * -> * 

Este es un ejemplo de un tipo alto.

Tenga en cuenta que podemos describir a Person y MaybePerson con los siguientes datos únicos de alto grado :

 data Person' f = Person { pName :: f String , pAge :: f Int } 

Aquí parametrizamos Persona ' sobre algo f (con género * -> * ), lo que nos permite hacer lo siguiente para usar los tipos originales:

 type Person = Person' Identity type MaybePerson = Person' Maybe 

Aquí usamos un tipo de identidad de contenedor simple

 -- already in Prelude newtype Identity a = Identity { runIdentity :: a } 

Aunque esto funciona, es un poco molesto en el caso de Person , ya que ahora todos nuestros datos están envueltos dentro de Identity :

 ghci> :t pName @Identity pName :: Person -> Identity String ghci> :t runIdentity. pName runIdentity. pName :: Person -> String 

Podemos eliminar esta molestia trivialmente, después de lo cual examinaremos por qué tal definición de Persona es realmente útil. Para deshacernos de los identificadores, podemos usar una familia de tipos (una función a nivel de tipo) que los borra:

 {-# LANGUAGE TypeFamilies #-} -- "Higher-Kinded Data" type family HKD fa where HKD Identity a = a HKD fa = fa data Person' f = Person  { pName :: HKD f String  , pAge :: HKD f Int  } deriving (Generic) 

Necesitamos la conclusión de Genérico para la segunda parte del artículo.

El uso de la familia de tipos HKD significa que el GHC borra automáticamente cualquier envoltorio de identidad en nuestras vistas:

 ghci> :t pName @Identity pName :: Person -> String ghci> :t pName @Maybe pName :: Person -> Maybe String 

y es precisamente esta versión de Persona del más alto tipo la que se puede usar de la mejor manera como un reemplazo para un reemplazo de nuestro original.

La pregunta obvia es qué nos compramos con todo este trabajo realizado. Volvamos a la redacción de validación para ayudarnos a responder esta pregunta.

Ahora podemos reescribirlo usando nuestra nueva técnica:

 validate :: Person' Maybe -> Maybe Person validate (Person name age) = Person <$> name <*> age 

¿No es un cambio muy interesante? Pero la intriga es lo poco que se necesita cambiar. Como puede ver, solo nuestro tipo y patrón coinciden con nuestra implementación inicial. Lo que está bien aquí es que ahora hemos consolidado Person y MaybePerson en la misma opinión, y por lo tanto ya no están conectados solo en el sentido nominal.

Genéricos y la función de validación más general


La versión actual de la función de validación debe escribirse para cada nuevo tipo de datos, aunque el código sea bastante rutinario.

Podemos escribir una versión de validación que funcione para cualquier tipo de datos superior.

Se podría usar Template Haskell, pero genera código y se usa solo en casos extremos. No lo haremos.

El secreto es contactar a GHC.Generics . Si no está familiarizado con la biblioteca, proporciona un isomorfismo del tipo de datos regular de Haskell a una representación general que puede ser controlada estructuralmente por un programador inteligente (es decir: nosotros). Al proporcionar código para que cambiemos los tipos, trabajos y coproductos constantes, podemos forzar GHC escribe un código independiente del tipo para nosotros. Esta es una técnica muy ordenada que le hará cosquillas en los dedos de los pies si no la ha visto antes.

Al final, queremos obtener algo como:

 validate :: _ => d Maybe -> Maybe (d Identity) 

Desde el punto de vista de los genéricos, cualquier tipo se puede dividir más comúnmente en varios diseños:

 -- undefined data, lifted version of Empty data V1 p -- Unit: used for constructors without arguments, lifted version of () data U1 p = U1 -- a container for ac, Constants, additional parameters and recursion of kind * newtype K1 icp = K1 { unK1 :: c } -- a wrapper, Meta-information (constructor names, etc.) newtype M1 itfp = M1 { unM1 :: fp } -- Sums: encode choice between constructors, lifted version of Either data (:+:) fgp = L1 (fp) | R1 (gp) -- Products: encode multiple arguments to constructors, lifted version of (,) data (:*:) fgp = (fp) :*: (gp) 

Es decir, pueden existir estructuras no inicializadas, estructuras libres de argumentos, estructuras constantes, metainformativas (constructores, etc.). Y también asociaciones de estructuras: total o asociaciones del tipo OR-OR y animadas, también son asociaciones o registros de forma corta.

Primero, necesitamos definir una clase que será el caballo de batalla de nuestra transformación. Por experiencia, esta es siempre la parte más difícil: los tipos de estas transformaciones generalizadas son extremadamente abstractos y, en mi opinión, muy difíciles de razonar. Vamos a usar:

 {-# LANGUAGE MultiParamTypeClasses #-} class GValidate io where gvalidate :: ip -> Maybe (op) 

Puede usar las reglas "suaves y lentas" para razonar sobre cómo debería verse su tipo de clase, pero en general, necesitará tanto un parámetro de entrada como uno de salida. Ambos deben ser del género * -> * , y luego transmitir esta p existencializada, debido a razones oscuras e impías que la humanidad no conoce. Luego, usando una pequeña lista de verificación, lo revisamos para ayudar a comprender nuestro terrible paisaje infernal, que veremos más adelante en secuencia.

En cualquier caso, nuestra clase ya está en nuestras manos, ahora solo necesitamos escribir instancias de nuestra clase para diferentes tipos de GHC . Podemos comenzar con el caso base, que debemos poder verificar, a saber, Quizás k :

 {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeOperators #-} instance GValidate (K1 a (Maybe k)) (K1 ak) where -- gvalidate :: K1 a (Maybe k) -> Maybe (K1 ak) gvalidate (K1 k) = K1 <$> k {-# INLINE gvalidate #-} 

K1 es un "tipo constante", lo que significa que aquí es donde termina nuestra recursividad estructural. En el ejemplo con nuestra Persona ', será pName :: HKD f String .

En la mayoría de los casos, cuando tiene un caso base, el resto son instancias definidas mecánicamente para otros tipos. A menos que necesite acceso a metadatos sobre el tipo de fuente en cualquier lugar, estas instancias casi siempre serán homomorfismos triviales.

Podemos comenzar con estructuras multiplicativas: si tenemos GValidate io y GValidate i 'o' , deberíamos poder ejecutarlas en paralelo:

 instance (GValidate io, GValidate i' o') => GValidate (i :*: i') (o :*: o') where gvalidate (l :*: r) = (:*:) <$> gvalidate l <*> gvalidate r {-# INLINE gvalidate #-} 

Si K1 se refiere directamente a los selectores de nuestra Persona , (: * :) corresponde aproximadamente a la sintaxis de la coma, que separamos nuestros campos en el registro.

Podemos definir una instancia de GValidate similar para coproductos o estructuras de resumen (los valores correspondientes están separados | en la definición de datos):

 instance (GValidate io, GValidate i' o') => GValidate (i :+: i') (o :+: o') where gvalidate (L1 l) = L1 <$> gvalidate l gvalidate (R1 r) = R1 <$> gvalidate r {-# INLINE gvalidate #-} 

Además, dado que no nos importa encontrar metadatos, simplemente podemos definir GValidate io sobre el constructor de metadatos:

 instance GValidate io => GValidate (M1 _a _b i) (M1 _a' _b' o) where gvalidate (M1 x) = M1 <$> gvalidate x {-# INLINE gvalidate #-} 

Ahora nos quedan estructuras sin interés para una descripción completa. Les proporcionaremos las siguientes instancias triviales para tipos no residenciales ( V1 ) y para diseñadores sin ningún parámetro ( U1 ):

 instance GValidate V1 V1 where gvalidate = undefined {-# INLINE gvalidate #-} instance GValidate U1 U1 where gvalidate U1 = Just U1 {-# INLINE gvalidate #-} 

El uso de indefinido es seguro aquí, ya que solo se puede llamar con un valor de V1 . Afortunadamente para nosotros, V1 está deshabitado y sin inicializar, por lo que esto nunca puede suceder, lo que significa que estamos moralmente en lo correcto en nuestro uso de indefinido .

Sin más preámbulos, ahora que tenemos todo este mecanismo, finalmente podemos escribir una versión no general de validación:

 {-# LANGUAGE FlexibleContexts #-} validate :: ( Generic (f Maybe) , Generic (f Identity) , GValidate (Rep (f Maybe)) (Rep (f Identity)) ) => f Maybe -> Maybe (f Identity) validate = fmap to . gvalidate . from 

Cada vez puede obtener una amplia sonrisa cuando la firma de la función es más larga que la implementación real; Esto significa que contratamos un compilador para escribir código para nosotros. Lo importante aquí para la validación es que no menciona Persona ' ; Esta función funcionará para cualquier tipo definido como datos de alta calidad. Voila!

Resumen


Eso es todo por hoy chicos. Nos familiarizamos con la idea de datos de alta calidad, vimos cómo es completamente equivalente al tipo de datos definido de una manera más tradicional, y también vimos qué cosas son posibles con este enfoque.

Le permite hacer todo tipo de cosas asombrosas, como: generar lentes para tipos de datos arbitrarios sin recurrir a Template Haskell; secuencia por tipo de datos; y rastrea automáticamente las dependencias para usar los campos de registro.

¡Feliz aplicación del parto alto!

Original: datos de mayor grado

Source: https://habr.com/ru/post/es429104/


All Articles