Dados altos

Sim, sim, você não sonhou e ouviu direito - é de alta qualidade. Tipo é um termo da teoria de tipos que significa essencialmente um tipo de tipo [dados].

Mas primeiro, algumas letras.

Vários artigos foram publicados no Habré, que descreviam detalhadamente o método de validação de dados em linguagens funcionais.

Este artigo é meus cinco centavos neste hype. Consideraremos a validação de dados em Haskell.

Validação de tipo


Um exemplo de uma técnica de validação usando validação de tipo foi considerado anteriormente:

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

Usando esse método, é simplesmente impossível criar dados incorretos. No entanto, apesar de essa validação ser muito simples de criar e ler, usá-la obriga a escrever muita rotina e fazer muitas alterações no código. Isso significa que o uso desse método é limitado apenas para dados realmente importantes.

Alta validação de dados




Neste artigo, veremos um método de validação diferente - usando dados de alta qualidade.

Suponha que tenhamos um tipo de dados:

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

E validaremos os dados apenas se todos os campos do registro forem válidos.
Como o Haskell tem funcionalidade superior à maioria das linguagens funcionais, ele pode se livrar facilmente da maioria das rotinas.

Aqui é possível e, portanto, esse método é amplamente utilizado entre autores de bibliotecas em Haskell.

Para fins de discussão, vamos imaginar que queremos que um usuário preencha informações pessoais por meio de um formulário da Web ou de qualquer outra coisa. Em outras palavras, é possível que eles possam prejudicar o preenchimento de parte da informação, não necessariamente invalidando a estrutura de dados restante. Se eles preencheram com êxito toda a estrutura, gostaríamos de receber um pincel Person preenchido.

Uma maneira de modelar é usar um segundo tipo de dados:

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

onde, lembro que o tipo opcional é usado:

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

A partir daqui, a função de validação é bastante simples:

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

Um pouco mais de detalhes sobre as funções (<$>) e (<*>)
Função (<$>) é apenas um sinônimo de infixo para fmap Functor

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

AND (<*>) é uma função da aplicação do Functor Aplicável

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

E para um tipo opcional, essas funções têm a seguinte definição

 -- 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 


Nossa validação funciona, mas é irritante escrever código de rotina adicional à mão, pois isso é feito completamente mecanicamente. Além disso, a duplicação desses esforços significa que precisaremos usar nossos cérebros no futuro para garantir que as três definições permaneçam sincronizadas. Seria ótimo se o compilador pudesse lidar com isso?

SURPRESA! ELE PODE! Uma família alta nos ajudará!

Em Haskell, existe um gênero, é um tipo , e a explicação mais simples e precisa é que um gênero é um tipo de tipo [data]. O gênero mais utilizado é o * , que pode ser chamado de "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] :: * 

E que tipo de talvez ?
 ghci> :k Maybe Maybe :: * -> * ghci> :k [] [] :: * -> * 

Este é um exemplo de alta qualidade.

Observe que podemos descrever Person e MaybePerson com os seguintes dados únicos e de alta qualidade :

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

Aqui, parametrizamos Person ' sobre algo f (com gênero * -> * ), que nos permite fazer o seguinte para usar os tipos originais:

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

Aqui usamos um tipo simples de identidade de wrapper

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

Embora isso funcione, é um pouco chato no caso de Person , pois agora todos os nossos dados estão agrupados dentro do Identity :

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

Podemos eliminar esse aborrecimento trivialmente, após o qual examinaremos por que essa definição de Pessoa 'é realmente útil. Para se livrar dos identificadores, podemos usar uma família de tipos (uma função no nível do tipo) que os apaga:

 {-# 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) 

Precisamos da conclusão do Generic para a 2ª parte do artigo.

Usar a família de tipos HKD significa que o GHC apaga automaticamente qualquer invólucro do Identity em nossas visões:

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

e é precisamente esta versão da Pessoa do tipo mais alto que pode ser usada da melhor maneira como um substituto para o nosso original.

A pergunta óbvia é o que compramos para nós mesmos com todo esse trabalho realizado. Vamos voltar à redação da validação para nos ajudar a responder a esta pergunta.

Agora podemos reescrevê-lo usando nossa nova técnica:

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

Não é uma mudança muito interessante? Mas a intriga é quão pouco precisa ser mudado. Como você pode ver, apenas nosso tipo e padrão correspondem à nossa implementação inicial. O que é interessante aqui é que agora consolidamos Person e MaybePerson na mesma visão e, portanto, eles não estão mais conectados apenas no sentido nominal.

Genéricos e a função de validação mais geral


A versão atual da função de validação precisa ser gravada para cada novo tipo de dados, mesmo que o código seja bastante rotineiro.

Podemos escrever uma versão de validação que funcione para qualquer tipo de dados mais alto.

Pode-se usar o Template Haskell, mas gera código e é usado apenas em casos extremos. Nós não vamos.

O segredo é entrar em contato com o GHC.Generics . Se você não estiver familiarizado com a biblioteca, ela fornece um isomorfismo do tipo de dados regular Haskell para uma representação geral que pode ser estruturalmente controlada por um programador inteligente (ou seja: nós.) Ao fornecer código para que possamos alterar tipos, trabalhos e coprodutos constantes, podemos forçar O GHC escreve um código independente de tipo para nós. Esta é uma técnica muito elegante que fará cócegas nos dedos dos pés, se você nunca a viu antes.

No final, queremos obter algo como:

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

Do ponto de vista de genéricos, qualquer tipo pode ser dividido em vários designs:

 -- 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) 

Ou seja, podem existir estruturas não inicializadas, estruturas sem argumentos, estruturas constantes, meta-informacionais (construtores, etc.). E também associações de estruturas - total ou associações do tipo OR-OR e animadas, também são associações ou registros abreviados.

Primeiro, precisamos definir uma classe que será o cavalo de batalha da nossa transformação. Por experiência, essa é sempre a parte mais difícil - os tipos dessas transformações generalizadas são extremamente abstratas e, na minha opinião, muito difíceis de raciocinar. Vamos usar:

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

Você pode usar as regras “soft and slow” para raciocinar sobre a aparência do seu tipo de classe, mas, em geral, você precisará de um parâmetro de entrada e de saída. Ambos devem ser do gênero * -> * , e depois transmitir esse p existencializado, devido a razões obscuras e profanas que a humanidade não conhece. Em seguida, usando uma pequena lista de verificação, passamos a ajudar a envolver nossa cabeça nessa terrível paisagem infernal, a qual abordaremos mais tarde em seguida.

De qualquer forma, nossa classe já está em nossas mãos, agora precisamos escrever instâncias de nossa classe para diferentes tipos de GHC.Generic . Podemos começar com o caso base, que devemos ser capazes de verificar, a saber Talvez 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 é um "tipo constante", o que significa que é aqui que nossa recursão estrutural termina. No exemplo com nossa Person ', será pName :: HKD f String .

Na maioria dos casos, quando você tem um caso base, o restante são apenas instâncias definidas mecanicamente para outros tipos. A menos que você precise acessar metadados sobre o tipo de fonte em qualquer lugar, essas instâncias quase sempre serão homomorfismos triviais.

Podemos começar com estruturas multiplicativas - se tivermos GValidate io e GValidate i 'o' , poderemos executá-los em paralelo:

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

Se K1 se referir diretamente aos seletores de nossa Pessoa , (: * :) corresponde aproximadamente à sintaxe da vírgula, que separamos nossos campos no registro.

Podemos definir uma instância GValidate semelhante para coprodutos ou estruturas de resumo (os valores correspondentes são separados | na definição de dados):

 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 #-} 

Além disso, como não nos preocupamos em encontrar metadados, podemos simplesmente definir o GValidate io sobre o construtor de metadados:

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

Agora ficamos com estruturas desinteressantes para uma descrição completa. Forneceremos a eles as seguintes instâncias triviais para tipos não residenciais ( V1 ) e para projetistas sem nenhum parâmetro ( U1 ):

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

Usar indefinido é seguro aqui, pois só pode ser chamado com um valor de V1 . Felizmente para nós, a V1 é desabitada e não inicializada, de modo que isso nunca pode acontecer, o que significa que estamos moralmente certos ao usar indefinidamente .

Sem mais delongas, agora que temos todo esse mecanismo, podemos finalmente escrever uma versão não geral da validação:

 {-# 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 que você pode obter um sorriso largo quando a assinatura da função é maior que a implementação real; isso significa que contratamos um compilador para escrever código para nós. O que é importante aqui para a validação é que ela não menciona Person ' ; essa função funcionará para qualquer tipo definido como dados de alta qualidade. Voila!

Sumário


Isso é tudo por hoje pessoal. Nós nos familiarizamos com a idéia de dados de alta qualidade, vimos como eles são completamente equivalentes ao tipo de dados definido de uma maneira mais tradicional e também vislumbramos o que é possível com essa abordagem.

Ele permite que você faça todos os tipos de coisas incríveis, como: gerar lentes para tipos de dados arbitrários sem recorrer ao Template Haskell; sequência por tipo de dados; e rastreia automaticamente dependências para o uso de campos de registro.

Feliz aplicação de parto elevado!

Original: Dados de Tipo Superior

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


All Articles