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:
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
AND
(<*>) é uma função da aplicação do Functor Aplicável
E para um tipo opcional, essas funções têm a seguinte definição
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
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 #-}
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:
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
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