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:
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
AND
(<*>) ist eine Funktion zum Anwenden des Applicative Functor
Für einen optionalen Typ haben diese Funktionen die folgende Definition
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
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 #-}
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:
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
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