Oui, oui, vous n’avez pas rêvé et vous avez bien entendu - c’est de haut niveau. Kind est un terme de théorie des types qui signifie essentiellement un type de type [données].
Mais d'abord, quelques paroles.
Plusieurs articles ont été publiés sur Habré, qui décrivaient en détail la méthode de validation des données dans les langages fonctionnels.
Cet article est mon cinq cents dans ce battage médiatique. Nous considérerons la validation des données dans Haskell.
Validation de type
Un exemple de technique de validation utilisant la validation de type a déjà été considéré:
type EmailContactInfo = String type PostalContactInfo = String data ContactInfo = EmailOnly EmailContactInfo | PostOnly PostalContactInfo | EmailAndPost (EmailContactInfo, PostalContactInfo) data Person = Person { pName :: String, , pContactInfo :: ContactInfo, }
En utilisant cette méthode, il est tout simplement impossible de créer des données incorrectes. Cependant, malgré le fait qu'une telle validation est très simple à créer et à lire, son utilisation vous oblige à écrire beaucoup de routine et à apporter de nombreuses modifications au code. Cela signifie que l'utilisation d'une telle méthode n'est limitée que pour les données vraiment importantes.
Validation élevée des données

Dans cet article, nous examinerons une méthode de validation différente - en utilisant des données de haute qualité.
Supposons que nous ayons un type de données:
data Person = Person { pName :: String , pAge :: Int }
Et nous ne validerons les données que si tous les champs de l'enregistrement sont valides.
Étant donné que Haskell est supérieur en fonctionnalités à la plupart des langages fonctionnels, il peut facilement se débarrasser de la plupart des routines.
Ici, c'est possible et, par conséquent, cette méthode est largement utilisée par les auteurs des bibliothèques de Haskell.
À des fins de discussion, imaginons que nous voulons qu'un utilisateur remplisse des informations personnelles via un formulaire Web ou autre chose. En d'autres termes, il est possible qu'ils gâchent le remplissage d'une partie des informations, n'invalidant pas nécessairement la structure de données restante. S'ils ont rempli avec succès la structure entière, nous aimerions obtenir un pinceau
Personne rempli.
Une façon de modéliser consiste à utiliser un deuxième type de données:
data MaybePerson = MaybePerson { mpName :: Maybe String , mpAge :: Maybe Int }
où, je me souviens que le type facultatif est utilisé:
De lĂ , la fonction de validation est assez simple:
validate :: MaybePerson -> Maybe Person validate (MaybePerson name age) = Person <$> name <*> age
Un peu plus de détails sur les fonctions (<$>) et (<*>)La fonction
(<$>) n'est qu'un infixe synonyme de
fmap Functor
ET
(<*>) est une fonction de l'application du foncteur d'application
Et pour un type facultatif, ces fonctions ont la définition suivante
Notre validation fonctionne, mais il est ennuyeux d'écrire du code de routine supplémentaire à la main, car cela se fait de manière complètement mécanique. De plus, la duplication de ces efforts signifie que nous devrons utiliser notre cerveau à l'avenir pour nous assurer que les trois définitions restent synchronisées. Serait-ce formidable si le compilateur pouvait gérer cela?
SURPRISE! IL PEUT! Une grande famille nous aidera!
Ă€ Haskell, il y a un genre, c'est aussi un
genre , et l'explication la plus simple et la plus précise est que le genre est un type de type [données]. Le genre le plus utilisé est
* , qui peut être appelé "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] :: *
Et quel genre de
Peut -
ĂŞtre ?
ghci> :k Maybe Maybe :: * -> * ghci> :k [] [] :: * -> *
Ceci est un exemple d'un type élevé.
Notez que nous pouvons décrire à la fois
Person et
MaybePerson avec les données uniques de haut
niveau suivantes :
data Person' f = Person { pName :: f String , pAge :: f Int }
Ici, nous paramétrons
Person ' sur quelque chose
f (avec gender
* -> * ), ce qui nous permet de faire ce qui suit afin d'utiliser les types originaux:
type Person = Person' Identity type MaybePerson = Person' Maybe
Ici, nous utilisons un type simple de wrapper d'identité
Bien que cela fonctionne, c'est un peu ennuyeux dans le cas de
Person , car maintenant toutes nos données sont enveloppées dans
Identity :
ghci> :t pName @Identity pName :: Person -> Identity String ghci> :t runIdentity. pName runIdentity. pName :: Person -> String
Nous pouvons éliminer cette gêne de manière triviale, après quoi nous examinerons pourquoi une telle définition de la
personne est vraiment utile. Pour se débarrasser des identifiants, nous pouvons utiliser une famille de types (une fonction au niveau du type) qui les efface:
{-# LANGUAGE TypeFamilies #-}
Nous avons besoin de la conclusion de
Generic pour la 2ème partie de l'article.
L'utilisation de la famille de types
HKD signifie que le GHC efface automatiquement tous
les wrappers d'
identité dans nos vues:
ghci> :t pName @Identity pName :: Person -> String ghci> :t pName @Maybe pName :: Person -> Maybe String
et c'est précisément cette version de
Person du plus haut type qui peut être utilisée de la meilleure façon en remplacement d'un remplacement pour notre version originale.
La question évidente est ce que nous avons acheté pour nous-mêmes avec tout ce travail effectué. Revenons au libellé de validation pour nous aider à répondre à cette question.
Nous pouvons maintenant le réécrire en utilisant notre nouvelle technique:
validate :: Person' Maybe -> Maybe Person validate (Person name age) = Person <$> name <*> age
Pas un changement très intéressant? Mais l'intrigue est le peu de choses à changer. Comme vous pouvez le voir, seuls notre type et notre modèle correspondent à notre implémentation initiale. Ce qui est bien ici, c'est que nous avons maintenant consolidé
Personne et
Peut-être Personne dans la même vue, et donc ils ne sont plus connectés uniquement au sens nominal.
Génériques et fonction de validation plus générale
La version actuelle de la fonction de validation doit être écrite pour chaque nouveau type de données, même si le code est assez routinier.
Nous pouvons écrire une version de validation qui fonctionnera pour tout type de données supérieur.
On pourrait utiliser Template Haskell, mais il génère du code et n'est utilisé que dans des cas extrêmes. Nous ne le ferons pas.
Le secret est de contacter
GHC.Generics . Si vous n'êtes pas familier avec la bibliothèque, elle fournit un isomorphisme du type de données régulier Haskell à une représentation générale qui peut être structurellement contrôlée par un programmeur intelligent (c'est-à -dire: nous.) En fournissant du code afin que nous puissions changer les types constants, les travaux et les coproduits, nous pouvons forcer GHC écrit pour nous un code indépendant du type. C'est une technique très soignée qui vous chatouillera les orteils si vous ne l'avez pas encore vue.
En fin de compte, nous voulons obtenir quelque chose comme:
validate :: _ => d Maybe -> Maybe (d Identity)
Du point de vue des
génériques, tout type peut généralement être divisé en plusieurs conceptions:
Autrement dit, il peut exister des structures non initialisées, des structures sans argument, des structures constantes, des méta-informations (constructeurs, etc.). Et aussi des associations de structures - totales ou associations de type OU-OU et animées, ce sont aussi des associations ou enregistrements abrégés.
Tout d'abord, nous devons définir une classe qui sera le cheval de bataille de notre transformation. D'après l'expérience, c'est toujours la partie la plus difficile - les types de ces transformations généralisées sont extrêmement abstraits et, à mon avis, très difficiles à raisonner. Utilisons:
{-# LANGUAGE MultiParamTypeClasses #-} class GValidate io where gvalidate :: ip -> Maybe (op)
Vous pouvez utiliser les règles «douces et lentes» pour raisonner sur l'apparence de votre type de classe, mais en général, vous aurez besoin à la fois d'un paramètre d'entrée et d'un paramètre de sortie. Les deux doivent appartenir au genre
* -> * , puis transmettre ce
p existentialisé, pour des raisons sombres et impies inconnues de l'humanité. Ensuite, à l'aide d'une petite liste de contrôle, nous passons en revue pour nous aider à comprendre ce terrible paysage infernal, que nous parcourrons plus tard dans l'ordre.
Dans tous les cas, notre classe est déjà entre nos mains, il nous suffit maintenant d'écrire des instances de notre classe pour différents types de
GHC.Generic . Nous pouvons commencer par le cas de base, que nous devons être en mesure de vérifier, à savoir
Peut-ĂŞtre k :
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeOperators #-} instance GValidate (K1 a (Maybe k)) (K1 ak) where
K1 est un "type constant", ce qui signifie que c'est là que se termine notre récursion structurelle. Dans l'exemple avec notre
personne ', ce sera
pName :: HKD f String .
Dans la plupart des cas, lorsque vous avez un scénario de base, les autres ne sont que des instances définies mécaniquement pour d'autres types. À moins que vous n'ayez besoin d'accéder aux métadonnées sur le type de source n'importe où, ces instances seront presque toujours des homomorphismes triviaux.
Nous pouvons commencer avec des structures multiplicatives - si nous avons
GValidate io et
GValidate i 'o' , nous devrions pouvoir les exécuter en parallèle:
instance (GValidate io, GValidate i' o') => GValidate (i :*: i') (o :*: o') where gvalidate (l :*: r) = (:*:) <$> gvalidate l <*> gvalidate r {-# INLINE gvalidate #-}
Si
K1 se réfère directement aux sélecteurs de notre
personne , (: * :) correspond approximativement à la syntaxe de la virgule, que nous séparons nos champs dans l'enregistrement.
Nous pouvons définir une instance
GValidate similaire pour les coproduits ou les structures récapitulatives (les valeurs correspondantes sont séparées
| dans la définition des données):
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 #-}
De plus, comme nous ne nous soucions pas de trouver des métadonnées, nous pouvons simplement définir
GValidate io sur le constructeur de métadonnées:
instance GValidate io => GValidate (M1 _a _b i) (M1 _a' _b' o) where gvalidate (M1 x) = M1 <$> gvalidate x {-# INLINE gvalidate #-}
Il nous reste maintenant des structures inintéressantes pour une description complète. Nous leur fournirons les instances triviales suivantes pour les types non résidentiels (
V1 ) et pour les concepteurs sans aucun paramètre (
U1 ):
instance GValidate V1 V1 where gvalidate = undefined {-# INLINE gvalidate #-} instance GValidate U1 U1 where gvalidate U1 = Just U1 {-# INLINE gvalidate #-}
L'utilisation d'
undefined est sûre ici, car elle ne peut être appelée qu'avec une valeur de
V1 . Heureusement pour nous,
V1 est inhabité et non initialisé, donc cela ne peut jamais arriver, ce qui signifie que nous avons moralement raison dans notre utilisation de
non défini .
Sans plus tarder, maintenant que nous avons tout ce mécanisme, nous pouvons enfin écrire une version non générale de la validation:
{-# 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
Chaque fois, vous pouvez obtenir un large sourire lorsque la signature de la fonction est plus longue que la mise en œuvre réelle; cela signifie que nous avons engagé un compilateur pour écrire du code pour nous. Ce qui est important ici pour la validation, c'est qu'il ne mentionne pas
Personne ' ; cette fonction fonctionnera pour tout type défini comme des données de haute qualité. Voila!
Résumé
C'est tout pour les gars d'aujourd'hui. Nous nous sommes familiarisés avec l'idée de données de haute qualité, avons vu comment elles sont complètement équivalentes au type de données défini de manière plus traditionnelle, et avons également entrevu les possibilités de cette approche.
Il vous permet de faire toutes sortes de choses étonnantes, telles que: générer des lentilles pour des types de données arbitraires sans avoir recours à Template Haskell;
séquence par type de données; et suivre automatiquement les dépendances pour l'utilisation des champs d'enregistrement.
Bonne application de l'accouchement élevé!
Original: données de type supérieur