Hohe Daten

Ja, ja, du hast nicht geträumt und richtig gehört - es ist von hoher Art. Art ist ein typentheoretischer Begriff, der im Wesentlichen eine Art von Typ [Daten] bedeutet.

Aber zuerst einige Texte.

Auf Habré wurden mehrere Artikel veröffentlicht, in denen die Methode der Datenvalidierung in funktionalen Sprachen ausführlich beschrieben wurde.

Dieser Artikel ist meine fünf Cent in diesem Hype. Wir werden die Datenvalidierung in Haskell in Betracht ziehen.

Typüberprüfung


Ein Beispiel für eine Validierungstechnik unter Verwendung der Typvalidierung wurde zuvor betrachtet:

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

Mit dieser Methode ist es einfach unmöglich, falsche Daten zu erstellen. Trotz der Tatsache, dass eine solche Validierung sehr einfach zu erstellen und zu lesen ist, müssen Sie bei ihrer Verwendung viel Routine schreiben und viele Änderungen am Code vornehmen. Dies bedeutet, dass die Verwendung einer solchen Methode nur für wirklich wichtige Daten beschränkt ist.

Hohe Datenvalidierung




In diesem Artikel werden wir uns mit einer anderen Validierungsmethode befassen - unter Verwendung hochwertiger Daten.

Angenommen, wir haben einen Datentyp:

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

Und wir werden die Daten nur validieren, wenn alle Felder des Datensatzes gültig sind.
Da Haskell den meisten funktionalen Sprachen in seiner Funktionalität überlegen ist, kann er die meisten Routinen leicht loswerden.

Hier ist es möglich und daher ist diese Methode unter Autoren von Bibliotheken in Haskell weit verbreitet.

Stellen wir uns zu Diskussionszwecken vor, dass ein Benutzer persönliche Informationen über ein Webformular oder etwas anderes ausfüllen soll. Mit anderen Worten, es ist möglich, dass sie das Ausfüllen eines Teils der Informationen verderben können, wodurch der Rest der Datenstruktur nicht unbedingt ungültig wird. Wenn sie die gesamte Struktur erfolgreich gefüllt haben, möchten wir einen gefüllten Personenpinsel erhalten .

Eine Möglichkeit zum Modellieren besteht darin, einen zweiten Datentyp zu verwenden:

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

Ich erinnere mich, dass der optionale Typ verwendet wird:

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

Von hier aus ist die Validierungsfunktion ganz einfach:

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

Ein bisschen mehr Details zu den Funktionen (<$>) und (<*>)
Die Funktion (<$>) ist nur ein Infix-Synonym für fmap Functor

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

AND (<*>) ist eine Funktion zum Anwenden des Applicative Functor

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

Für einen optionalen Typ haben diese Funktionen die folgende Definition

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


Unsere Validierung funktioniert, aber es ist ärgerlich, zusätzlichen Routinecode von Hand zu schreiben, da dies vollständig mechanisch erfolgt. Darüber hinaus bedeutet die Verdoppelung dieser Bemühungen, dass wir in Zukunft unser Gehirn einsetzen müssen, um sicherzustellen, dass alle drei Definitionen synchron bleiben. Wäre es toll, wenn der Compiler damit umgehen könnte?

ÜBERRASCHUNG! ER KANN! Eine große Familie wird uns helfen!

In Haskell gibt es so etwas wie eine Gattung, es ist eine Art , und die einfachste und genaueste Erklärung ist, dass eine Gattung eine Art von Typ [Daten] ist. Die am weitesten verbreitete Gattung ist * , die als "final" bezeichnet werden kann.

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

Und was für ein Vielleicht ?
 ghci> :k Maybe Maybe :: * -> * ghci> :k [] [] :: * -> * 

Dies ist ein Beispiel von hoher Art.

Beachten Sie, dass wir sowohl Person als auch MaybePerson mit den folgenden einzelnen, hochwertigen Daten beschreiben können:

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

Hier parametrisieren wir Person ' über etwas f (mit Geschlecht * -> * ), wodurch wir Folgendes tun können, um die ursprünglichen Typen zu verwenden:

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

Hier verwenden wir einen einfachen Wrapper-Typ von Identität

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

Obwohl dies funktioniert, ist es im Fall von Person etwas ärgerlich, da jetzt alle unsere Daten in Identity verpackt sind:

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

Wir können diesen Ärger trivial beseitigen, woraufhin wir untersuchen werden, warum eine solche Definition von Person wirklich nützlich ist. Um Bezeichner zu entfernen, können wir eine Typenfamilie (eine Funktion auf Textebene) verwenden, die sie löscht:

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

Wir brauchen den Abschluss von Generic für den 2. Teil des Artikels.

Die Verwendung der HKD-Typfamilie bedeutet, dass der GHC alle Identitäts- Wrapper in unseren Ansichten automatisch löscht:

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

und genau diese Version der Person der höchsten Art kann am besten als Ersatz für einen Ersatz für unsere ursprüngliche verwendet werden.

Die offensichtliche Frage ist, was wir uns bei all dieser Arbeit gekauft haben. Kehren wir zum Wortlaut der Validierung zurück, um diese Frage zu beantworten.

Wir können es jetzt mit unserer neuen Technik umschreiben:

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

Keine sehr interessante Änderung? Aber die Intrige ist, wie wenig geändert werden muss. Wie Sie sehen können, stimmen nur unser Typ und Muster mit unserer ursprünglichen Implementierung überein. Was hier ordentlich ist, ist, dass wir jetzt Person und MaybePerson in derselben Ansicht konsolidiert haben und sie daher nicht mehr nur im nominellen Sinne verbunden sind.

Generika und die allgemeinere Validierungsfunktion


Die aktuelle Version der Validierungsfunktion muss für jeden neuen Datentyp geschrieben werden, obwohl der Code ziemlich routinemäßig ist.

Wir können eine Validierungsversion schreiben, die für jeden höheren Datentyp funktioniert.

Man könnte Template Haskell verwenden, aber es generiert Code und wird nur in extremen Fällen verwendet. Wir werden nicht.

Das Geheimnis ist, GHC.Generics zu kontaktieren. Wenn Sie mit der Bibliothek nicht vertraut sind, bietet sie einen Isomorphismus vom regulären Datentyp Haskell zu einer allgemeinen Darstellung, die von einem intelligenten Programmierer (dh uns) strukturell gesteuert werden kann. Durch die Bereitstellung von Code, mit dem wir konstante Typen, Produkte und Nebenprodukte ändern, können wir erzwingen GHC schreibt einen typunabhängigen Code für uns. Dies ist eine sehr nette Technik, die Ihre Zehen kitzelt, wenn Sie sie noch nicht gesehen haben.

Am Ende wollen wir so etwas wie:

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

Aus Sicht von Generics kann jeder Typ am häufigsten in mehrere Designs unterteilt werden:

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

Das heißt, es können nicht initialisierte Strukturen, argumentfreie Strukturen, konstante Strukturen, Metainformationen (Konstruktoren usw.) existieren. Und auch Assoziationen von Strukturen - Gesamt oder Assoziationen vom Typ OR-OR und animiert, sind sie auch Kurzformassoziationen oder Datensätze.

Zuerst müssen wir eine Klasse definieren, die das Arbeitstier unserer Transformation sein wird. Aus Erfahrung ist dies immer der schwierigste Teil - die Arten dieser verallgemeinerten Transformationen sind äußerst abstrakt und meiner Meinung nach sehr schwer zu begründen. Verwenden wir:

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

Sie können die "weichen und langsamen" Regeln verwenden, um zu überlegen, wie Ihr Klassentyp aussehen soll. Im Allgemeinen benötigen Sie jedoch sowohl einen Eingabe- als auch einen Ausgabeparameter. Beide müssen der Gattung * -> * angehören und dann dieses existentialisierte p aus dunklen, unheiligen Gründen übertragen, die der Menschheit nicht bekannt sind. Dann gehen wir mit einer kleinen Checkliste durch, um unsere Köpfe um diese schreckliche höllische Landschaft zu wickeln, die wir später nacheinander umrunden werden.

In jedem Fall ist unsere Klasse bereits in unseren Händen, jetzt müssen wir nur noch Instanzen unserer Klasse für verschiedene Arten von GHC.Generic schreiben . Wir können mit dem Basisfall beginnen, den wir überprüfen müssen, nämlich Vielleicht 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 ist ein "konstanter Typ", was bedeutet, dass hier unsere strukturelle Rekursion endet. Im Beispiel mit unserer Person 'ist dies pName :: HKD f String .

In den meisten Fällen, wenn Sie einen Basisfall haben, sind die restlichen nur mechanisch definierte Instanzen für andere Typen. Sofern Sie nicht überall Zugriff auf Metadaten zum Quelltyp benötigen, handelt es sich bei diesen Instanzen fast immer um triviale Homomorphismen.

Wir können mit multiplikativen Strukturen beginnen - wenn wir GValidate io und GValidate i 'o' haben , sollten wir sie parallel ausführen können:

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

Wenn sich K1 direkt auf die Selektoren unserer Person bezieht, entspricht (: * :) ungefähr der Syntax des Kommas, durch das wir unsere Felder im Datensatz trennen.

Wir können eine ähnliche GValidate- Instanz für Nebenprodukte oder Zusammenfassungsstrukturen definieren (die entsprechenden Werte sind in der Datendefinition getrennt):

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

Da es uns nicht wichtig ist , Metadaten zu finden, können wir GValidate io einfach über den Metadatenkonstruktor definieren:

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

Jetzt bleiben uns uninteressante Strukturen für eine vollständige Beschreibung. Wir werden ihnen die folgenden trivialen Instanzen für Nichtwohnungsarten ( V1 ) und für Designer ohne Parameter ( U1 ) zur Verfügung stellen:

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

Die Verwendung von undefined ist hier sicher, da es nur mit einem Wert von V1 aufgerufen werden kann. Zum Glück ist V1 unbewohnt und nicht initialisiert, so dass dies niemals passieren kann, was bedeutet, dass wir moralisch richtig mit undefiniert umgehen .

Jetzt, da wir diesen ganzen Mechanismus haben, können wir ohne weiteres endlich eine nicht allgemeine Version der Validierung schreiben:

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

Jedes Mal können Sie ein breites Lächeln erhalten, wenn die Signatur für die Funktion länger als die tatsächliche Implementierung ist. Dies bedeutet, dass wir einen Compiler beauftragt haben, Code für uns zu schreiben. Was hier für die Validierung wichtig ist, ist, dass die Person nicht erwähnt wird. Diese Funktion funktioniert für jeden Typ, der als qualitativ hochwertige Daten definiert ist. Voila!

Zusammenfassung


Das ist alles für heute Jungs. Wir haben uns mit der Idee qualitativ hochwertiger Daten vertraut gemacht, festgestellt, dass sie dem auf traditionellere Weise definierten Datentyp vollständig entsprechen, und einen Blick darauf geworfen, was mit diesem Ansatz möglich ist.

Sie können damit alle Arten von erstaunlichen Dingen ausführen, z. B.: Objektive für beliebige Datentypen generieren, ohne auf Template Haskell zurückgreifen zu müssen; Reihenfolge nach Datentyp; und verfolgen automatisch Abhängigkeiten für die Verwendung von Datensatzfeldern.

Glückliche Anwendung der hohen Geburt!

Original: Höhere Daten

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


All Articles