Hochwertige Daten

Published on November 09, 2018

Hochwertige Daten

    Ja, du hast nicht geträumt, und du hast nicht falsch gehört - es ist eine hohe Art. Eine Art ist ein Begriff der Typentheorie, was im Wesentlichen den Typ des Datentyps bedeutet.

    Aber zuerst ein paar Texte.

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

    Dieser Artikel - meine fünf Cent in diesem HYIP. Wir werden uns die Datenvalidierung in Haskell ansehen.

    Typvalidierung


    Zuvor wurde ein Beispiel für eine Validierungstechnik unter Verwendung der Typvalidierung 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 viel Routine schreiben und viele Änderungen am Code vornehmen. Daher ist die Verwendung einer solchen Methode nur für wirklich wichtige Daten beschränkt.

    Validierung von Daten hoher Art




    In diesem Artikel werden wir uns eine andere Validierungsmethode ansehen - die Verwendung von Daten höherer Art.

    Angenommen, wir haben einen Datentyp:

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

    Die Daten werden nur dann überprüft, wenn alle Felder im Datensatz gültig sind.
    Da Haskell funktionaler ist als die meisten funktionalen Sprachen, können Sie die meisten Routinen problemlos entfernen.

    Hier ist es möglich und daher wird diese Methode von Bibliotheksautoren auf Haskell häufig verwendet.

    Stellen wir uns zu Diskussionszwecken vor, dass der Benutzer persönliche Informationen über ein Webformular oder etwas anderes eingeben soll. Mit anderen Worten, es ist möglich, dass sie das Füllen einiger Informationen beeinträchtigen, ohne dass der Rest der Datenstruktur gelöscht werden muss. Wenn sie die gesamte Struktur erfolgreich vervollständigen, möchten wir eine ausgefüllte Rekordperson .

    Eine der Modellierungsmethoden ist die Verwendung des zweiten Datentyps:

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

    Lassen Sie mich daran erinnern, dass Sie den optionalen Typ verwenden:

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

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

    Und (<*>) ist eine Funktion zum Anwenden eines anwendbaren Funktors

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

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

    -- already in Prelude
    (<$>) :: (a -> b) -> Maybe a -> Maybe b
    _ <$> Nothing   = Nothing
    f <$> (Just a)  = Just (f a)
    (<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
    (Just f) <*> m   = f <$> m
    Nothing  <*> _   = Nothing
    


    Unsere Validierung funktioniert, aber es ist ärgerlich, einen 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! Wir werden dem hohen Rennen helfen!

    In Haskell gibt es so etwas wie eine Gattung, es ist auch eine Art , und die einfachste und genaueste Erklärung ist, dass eine Gattung eine Art [Datentyp] ist. Die am weitesten verbreitete Gattung ist * , die als "endlich" 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 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 eindeutigen Daten eines hohen Geschlechts beschreiben können:

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

    Hier parametrisieren wir Person ' über etwas f (mit der Gattung * -> * ), was es uns ermöglicht, Folgendes zu tun, um die Quelltypen zu verwenden:

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

    Hier verwenden wir einen einfachen Wrapper vom Typ 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 überlegen, warum eine solche Definition von Person wirklich nützlich ist. Um Identifikatoren loszuwerden, können wir die Typenfamilie (Funktion auf Typebene) verwenden, mit der sie gelöscht werden:

    {-# LANGUAGE TypeFamilies #-}
    -- "Higher-Kinded Data"
    type family HKD f a where
      HKD Identity a = a
      HKD f        a = f a
    data Person' f = Person
      { pName :: HKD f String
      , pAge  :: HKD f Int
      } deriving (Generic)

    Generische Ausgabe benötigen wir für den 2. Teil des Artikels.

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

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

    und es ist genau eine solche Version von Person einer hohen Art, die am besten als Ersatz für unsere ursprüngliche verwendet werden kann.

    Die offensichtliche Frage ist, dass wir uns mit all dieser Arbeit eingekauft haben. Kehren wir zum Validierungswortlaut zurück, um uns bei der Beantwortung dieser Frage zu helfen.

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

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

    Keine sehr interessante Veränderung? Aber die Intrige ist, wie wenig Sie ändern müssen. Wie Sie sehen, stimmen nur Typ und Muster mit unserer ursprünglichen Implementierung überein. Ordentlich ist hier, dass wir Person und MaybePerson nun in derselben Darstellung zusammengefasst haben und sie daher nicht mehr nur im nominalen Sinne miteinander verwandt sind.

    Generics und 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.

    Es wäre möglich , eine Vorlage Haskell (verwenden TemplateHaskell ), aber es erzeugt den Code und nur in Extremfällen eingesetzt wird. Wir werden nicht.

    Das Geheimnis ist, sich an GHC.Generics zu wenden. Wenn Sie mit der Bibliothek nicht vertraut sind, bietet sie einen Isomorphismus vom regulären Haskell-Datentyp in eine allgemeine Darstellung, die von einem intelligenten Programmierer (dh uns) strukturell gesteuert werden kann GHC schreibt für uns unabhängig vom Typ Code. Dies ist eine sehr saubere Technik, die Ihre Zehen kitzelt, wenn Sie es noch nicht gesehen haben.

    Wir wollen am Ende etwas haben wie:

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

    Aus Sicht von Generics kann jeder Typ im Allgemeinen in mehrere Strukturen 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 a c, Constants, additional parameters and recursion of kind *
    newtype K1    i c p = K1 { unK1 :: c } 
    -- a wrapper, Meta-information (constructor names, etc.)
    newtype M1  i t f p = M1 { unM1 :: f p } 
    -- Sums: encode choice between constructors, lifted version of Either
    data    (:+:) f g p = L1 (f p) | R1 (g p) 
    -- Products: encode multiple arguments to constructors, lifted version of (,)
    data    (:*:) f g p = (f p) :*: (g p) 

    Das heißt, es kann nicht liberalisierte Strukturen, Strukturen ohne Argumente, konstante Strukturen, Metainformationen (Konstruktoren usw.) geben. Neben Assoziationen von Strukturen - Total oder Assoziationen vom Typ OR-OR und Multiplikation - handelt es sich auch um Cortex-Assoziationen oder -Aufzeichnungen.

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

    {-# LANGUAGE MultiParamTypeClasses #-}
    
    class GValidate i o where
      gvalidate :: i p -> Maybe (o p)

    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 von der Art * -> * sein und dann dieses existentialisierte p übertragen , und zwar aus dunklen, unheiligen Gründen, die der Menschheit nicht bekannt sind. Dann gehen wir mit einer kleinen Checkliste durch, um diese albtraumhafte, höllische Landschaft, die wir später nacheinander erkunden werden, in den Kopf zu schließen.

    In jedem Fall ist unsere Klasse bereits in unseren Händen, jetzt müssen wir nur noch Kopien unserer Klasse für verschiedene Arten von GHC.Generic schreiben . Wir können mit dem Basisfall beginnen, den wir überprüfen sollten, nämlich: Vielleicht k :

    {-# LANGUAGE FlexibleInstances #-}
    {-# LANGUAGE TypeOperators     #-}
    
    instance GValidate (K1 a (Maybe k)) (K1 a k) where
      -- gvalidate :: K1 a (Maybe k) -> Maybe (K1 a k)
      gvalidate (K1 k) = K1 <$> k
      {-# INLINE gvalidate #-}

    K1 ist ein "konstanter Typ", was bedeutet, dass hier unsere strukturelle Rekursion endet. Im Beispiel unserer Person ist dies pName :: HKD f String .

    In den meisten Fällen, in denen Sie einen Basisfall haben, sind der Rest einfach mechanisch bestimmte Instanzen für andere Typen. Wenn Sie keinen Zugriff auf Metadaten zum Originaltyp 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 i o, GValidate i' o')
        => GValidate (i :*: i') (o :*: o') where
      gvalidate (l :*: r) = (:*:)
                        <$> gvalidate l
                        <*> gvalidate r
      {-# INLINE gvalidate #-}

    Wenn K1 direkt auf die Selektoren unserer Person verweist , entspricht (: * :) ungefähr der Kommasyntax, mit der wir unsere Felder im Datensatz trennen.

    Wir können eine ähnliche Instanz definieren GValidate für Coprodukten oder Gesamtstrukturen (Werte , die durch getrennt sind | in der Definition der Daten):

    instance (GValidate i o, 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 im Metadatenkonstruktor definieren :

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

    Nun gibt es uninteressante Strukturen für eine vollständige Beschreibung. Wir werden ihnen die folgenden unbedeutenden Kopien für Nichtwohntypen ( 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. Daher sind wir moralisch in der Verwendung von undefined richtig .

    Ohne weiteres können wir nun, da wir über diesen gesamten Mechanismus verfügen, endlich eine nicht generische 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, wenn die Signatur für die Funktion länger als die tatsächliche Implementierung ist, können Sie ein breites Lächeln erhalten. Das heißt, wir haben einen Compiler beauftragt, Code für uns zu schreiben. Was für die Validierung hier wichtig ist, ist, dass die Person nicht erwähnt wird . Diese Funktion kann für jeden Typ verwendet werden, der als High Data definiert ist. Voila!

    Ergebnisse


    Das ist alles für heute, Jungs. Wir haben die Idee der Daten höherer Ordnung kennengelernt, gesehen, wie diese Daten der Art der Daten entsprechen, die auf traditionellere Weise ermittelt wurden, und haben auch einen Blick darauf erhascht, was mit diesem Ansatz alles möglich ist.

    Sie können damit alle möglichen erstaunlichen Dinge tun, z. B .: Objektive für beliebige Datentypen generieren, ohne auf Pattern Haskel zurückgreifen zu müssen; Reihenfolge nach Datentyp; und automatisch nachverfolgen, ob Abhängigkeiten Datensatzfelder verwenden.

    Viel Spaß bei der Beantragung einer hohen Lieferung!

    Original: Höherwertige Daten