Haskell-Typ gegen Datenkonstruktor

122

Ich lerne Haskell von learnyouahaskell.com . Ich habe Probleme, Typkonstruktoren und Datenkonstruktoren zu verstehen. Zum Beispiel verstehe ich den Unterschied nicht wirklich:

data Car = Car { company :: String  
               , model :: String  
               , year :: Int  
               } deriving (Show) 

und das:

data Car a b c = Car { company :: a  
                     , model :: b  
                     , year :: c   
                     } deriving (Show)  

Ich verstehe, dass der erste einfach einen Konstruktor ( Car) verwendet, um Daten vom Typ zu erstellen Car. Ich verstehe den zweiten nicht wirklich.

Wie werden Datentypen wie folgt definiert:

data Color = Blue | Green | Red

in all das passen?

Von dem, was ich verstehe, das dritte Beispiel ( Colorist) ein Typ, der in drei Zuständen sein kann: Blue, Greenoder Red. Dies steht jedoch im Widerspruch zu meinem Verständnis der ersten beiden Beispiele: CarKann sich der Typ nur in einem Zustand befinden, Carfür dessen Erstellung verschiedene Parameter erforderlich sind? Wenn ja, wie passt das zweite Beispiel dazu?

Im Wesentlichen suche ich nach einer Erklärung, die die obigen drei Codebeispiele / Konstrukte vereinheitlicht.

Aristides
quelle
17
Ihr Auto-Beispiel kann etwas verwirrend sein, da Cares sich sowohl um einen Typkonstruktor (auf der linken Seite =) als auch um einen Datenkonstruktor (auf der rechten Seite) handelt. Im ersten Beispiel Carakzeptiert der Typkonstruktor keine Argumente, im zweiten Beispiel drei. In beiden Beispielen verwendet der CarDatenkonstruktor drei Argumente (die Typen dieser Argumente sind jedoch in einem Fall festgelegt und in dem anderen parametrisiert).
Simon Shine
Das erste ist einfach die Verwendung eines Datenkonstruktors ( Car :: String -> String -> Int -> Car), um Daten vom Typ zu erstellen Car. Der zweite verwendet einfach einen Datenkonstruktor ( Car :: a -> b -> c -> Car a b c), um Daten vom Typ zu erstellen Car a b c.
Will Ness

Antworten:

226

In einer dataErklärung, ein Typkonstruktor ist das , was auf der linken Seite des Gleichheitszeichens. Die Datenkonstruktoren sind die Dinge auf der rechten Seite des Gleichheitszeichens. Sie verwenden Typkonstruktoren, bei denen ein Typ erwartet wird, und Sie verwenden Datenkonstruktoren, bei denen ein Wert erwartet wird.

Datenkonstruktoren

Zur Vereinfachung können wir mit einem Beispiel eines Typs beginnen, der eine Farbe darstellt.

data Colour = Red | Green | Blue

Hier haben wir drei Datenkonstruktoren. Colourist ein Typ und Greenein Konstruktor, der einen Wert vom Typ enthält Colour. Ebenso Redund Bluesind beide Konstruktoren, die Werte vom Typ konstruieren Colour. Wir könnten uns vorstellen, es aufzupeppen!

data Colour = RGB Int Int Int

Wir haben nach wie vor nur die Art Colour, aber RGBist kein Wert - es ist eine Funktion drei Ints nehmen und Rückkehr Wert! RGBhat den Typ

RGB :: Int -> Int -> Int -> Colour

RGBist ein Datenkonstruktor, bei dem es sich um eine Funktion handelt, die einige Werte als Argumente verwendet und diese dann verwendet, um einen neuen Wert zu erstellen . Wenn Sie objektorientiert programmiert haben, sollten Sie dies erkennen. In OOP nehmen Konstruktoren auch einige Werte als Argumente und geben einen neuen Wert zurück!

In diesem Fall erhalten RGBwir einen Farbwert , wenn wir auf drei Werte anwenden !

Prelude> RGB 12 92 27
#0c5c1b

Wir haben einen Wert vom Typ Colourkonstruiert, indem wir den Datenkonstruktor angewendet haben. Ein Datenkonstruktor enthält entweder einen Wert wie eine Variable oder verwendet andere Werte als Argument und erstellt einen neuen Wert . Wenn Sie zuvor programmiert haben, sollte Ihnen dieses Konzept nicht sehr fremd sein.

Pause

Wenn Sie einen Binärbaum zum Speichern von Strings erstellen möchten , können Sie sich vorstellen, so etwas zu tun

data SBTree = Leaf String
            | Branch String SBTree SBTree

Was wir hier sehen, ist ein Typ SBTree, der zwei Datenkonstruktoren enthält. Mit anderen Worten, es gibt zwei Funktionen (nämlich Leafund Branch), die Werte des SBTreeTyps konstruieren . Wenn Sie nicht mit der Funktionsweise von Binärbäumen vertraut sind, bleiben Sie einfach dran. Sie müssen eigentlich nicht wissen, wie Binärbäume funktionieren, nur dass dieser Strings auf irgendeine Weise speichert .

Wir sehen auch, dass beide Datenkonstruktoren ein StringArgument annehmen - dies ist der String, den sie im Baum speichern werden.

Aber! Was wäre, wenn wir auch speichern könnten, müssten Boolwir einen neuen Binärbaum erstellen. Es könnte ungefähr so ​​aussehen:

data BBTree = Leaf Bool
            | Branch Bool BBTree BBTree

Typkonstruktoren

Beide SBTreeund BBTreesind Typkonstruktoren. Aber es gibt ein eklatantes Problem. Sehen Sie, wie ähnlich sie sind? Das ist ein Zeichen dafür, dass Sie wirklich irgendwo einen Parameter wollen.

Also können wir das tun:

data BTree a = Leaf a
             | Branch a (BTree a) (BTree a)

Jetzt führen wir eine Typvariable a als Parameter in den Typkonstruktor ein. In dieser Erklärung BTreeist eine Funktion geworden. Es nimmt einen Typ als Argument und gibt einen neuen Typ zurück .

Hier ist es wichtig , den Unterschied zwischen einem betrachten konkreter (Beispiele umfassen Int, [Char]und Maybe Bool) , die ein Typ ist, der auf einen Wert in Ihrem Programm zugeordnet werden kann, und eine Typkonstruktor Funktion , die Sie brauchen eine Art zu ernähren zu können sein einem Wert zugeordnet. Ein Wert kann niemals vom Typ "Liste" sein, da es sich um eine "Liste von etwas " handeln muss. Im gleichen Sinne kann ein Wert niemals vom Typ "Binärbaum" sein, da es sich um einen "Binärbaum handeln muss, der etwas speichert ".

Wenn wir Boolbeispielsweise als Argument an übergeben BTree, wird der Typ zurückgegeben BTree Bool, bei dem es sich um einen Binärbaum handelt, in dem Bools gespeichert ist . Ersetzen Sie jedes Vorkommen der Typvariablen adurch den Typ Bool, und Sie können selbst sehen, wie es wahr ist.

Wenn Sie möchten, können Sie BTreeals Funktion mit der Art anzeigen

BTree :: * -> *

Arten sind etwas wie Typen - das *zeigt einen konkreten Typ an, also sagen wir, es BTreehandelt sich von einem konkreten Typ zu einem konkreten Typ.

Einpacken

Treten Sie einen Moment zurück und beachten Sie die Ähnlichkeiten.

  • Ein Datenkonstruktor ist eine "Funktion", die 0 oder mehr Werte annimmt und Ihnen einen neuen Wert zurückgibt.

  • Ein Typkonstruktor ist eine "Funktion", die 0 oder mehr Typen akzeptiert und Ihnen einen neuen Typ zurückgibt.

Datenkonstruktoren mit Parametern sind cool, wenn wir geringfügige Abweichungen in unseren Werten wünschen. Wir setzen diese Abweichungen in den Parametern ein und lassen den Typ, der den Wert erstellt, entscheiden, welche Argumente sie eingeben. Im gleichen Sinne sind Typkonstruktoren mit Parametern cool wenn wir leichte Abweichungen in unseren Typen wollen! Wir setzen diese Variationen als Parameter und lassen den Typ, der den Typ erstellt, entscheiden, welche Argumente er eingeben wird.

Eine Fallstudie

Als Heimstrecke hier können wir den Maybe aTyp berücksichtigen . Seine Definition ist

data Maybe a = Nothing
             | Just a

Hier Maybeist ein Typkonstruktor, der einen konkreten Typ zurückgibt. Justist ein Datenkonstruktor, der einen Wert zurückgibt. Nothingist ein Datenkonstruktor, der einen Wert enthält. Wenn wir uns die Art von ansehen Just, sehen wir das

Just :: a -> Maybe a

Mit anderen Worten, Justnimmt einen Wert vom Typ an aund gibt einen Wert vom Typ zurück Maybe a. Wenn wir uns die Art von ansehen Maybe, sehen wir das

Maybe :: * -> *

Mit anderen Worten, Maybenimmt einen konkreten Typ und gibt einen konkreten Typ zurück.

Noch einmal! Der Unterschied zwischen einer konkreten Typ- und einer Typkonstruktorfunktion. Sie können keine Liste von Maybes erstellen - wenn Sie versuchen, auszuführen

[] :: [Maybe]

Sie erhalten eine Fehlermeldung. Sie können jedoch eine Liste von Maybe Intoder erstellen Maybe a. Das liegt daran, dass Maybees sich um eine Typkonstruktorfunktion handelt, eine Liste jedoch Werte eines konkreten Typs enthalten muss. Maybe Intund Maybe asind konkrete Typen (oder, wenn Sie möchten, Aufrufe, Typkonstruktorfunktionen einzugeben, die konkrete Typen zurückgeben.)

kqr
quelle
2
In Ihrem ersten Beispiel sind sowohl ROTGRÜN als auch BLAU Konstruktoren, die keine Argumente akzeptieren.
OllieB
3
Die Behauptung, dass in data Colour = Red | Green | Blue"Wir haben überhaupt keine Konstruktoren" einfach falsch ist. Typkonstruktoren und Datenkonstruktoren müssen keine Argumente annehmen, siehe z. B. haskell.org/haskellwiki/Constructor, in dem darauf hingewiesen wird, dass in data Tree a = Tip | Node a (Tree a) (Tree a)"Es gibt zwei Datenkonstruktoren, Tip und Node".
Frerich Raabe
1
@CMCDragonkai Du bist absolut richtig! Arten sind die "Arten von Typen". Ein üblicher Ansatz zum Verbinden der Konzepte von Typen und Werten wird als abhängige Typisierung bezeichnet . Idris ist eine von Haskell inspirierte, abhängig getippte Sprache. Mit den richtigen GHC-Erweiterungen können Sie auch der abhängigen Eingabe in Haskell etwas nahe kommen. (Einige Leute haben gescherzt, dass "in der Haskell-Forschung herausgefunden werden soll, wie nahe wir abhängigen Typen kommen können, ohne abhängige Typen zu haben.")
kqr
1
@CMCDragonkai Es ist tatsächlich nicht möglich, eine leere Datendeklaration in Standard-Haskell zu haben. Es gibt jedoch eine GHC-Erweiterung ( -XEmptyDataDecls), mit der Sie dies tun können. Da es, wie Sie sagen, keine Werte für diesen Typ gibt, kann eine Funktion f :: Int -> Zbeispielsweise niemals zurückkehren (denn was würde sie zurückgeben?). Sie können jedoch nützlich sein, wenn Sie Typen möchten, sich aber nicht wirklich um Werte kümmern .
kqr
1
Wirklich ist es nicht möglich? Ich habe es gerade in GHC versucht und es lief ohne Fehler. Ich musste keine GHC-Erweiterungen laden, nur Vanille-GHC. Ich konnte dann schreiben :k Zund es gab mir nur einen Stern.
CMCDragonkai
42

Haskell hat algebraische Datentypen , die nur sehr wenige andere Sprachen haben. Das verwirrt dich vielleicht.

In anderen Sprachen können Sie normalerweise einen "Datensatz", eine "Struktur" oder ähnliches erstellen, der eine Reihe benannter Felder enthält, die verschiedene Datentypen enthalten. Sie können manchmal auch eine „Aufzählung“, machen die einen (kleinen) Satz von festen möglichen Werten hat (zB Ihr Red, Greenund Blue).

In Haskell können Sie beide gleichzeitig kombinieren . Seltsam, aber wahr!

Warum heißt es "algebraisch"? Nun, die Nerds sprechen von "Summentypen" und "Produkttypen". Beispielsweise:

data Eg1 = One Int | Two String

Ein Eg1Wert ist im Grunde entweder eine Ganzzahl oder eine Zeichenfolge. Die Menge aller möglichen Eg1Werte ist also die "Summe" der Menge aller möglichen ganzzahligen Werte und aller möglichen Zeichenkettenwerte. Nerds werden daher Eg1als "Summentyp" bezeichnet. Andererseits:

data Eg2 = Pair Int String

Jeder Eg2Wert besteht sowohl aus einer Ganzzahl als auch aus einer Zeichenfolge. Die Menge aller möglichen Eg2Werte ist also das kartesische Produkt der Menge aller ganzen Zahlen und der Menge aller Zeichenfolgen. Die beiden Sätze werden miteinander "multipliziert", dies ist also ein "Produkttyp".

Die algebraischen Typen von Haskell sind Summentypen von Produkttypen . Sie geben einem Konstruktor mehrere Felder, um einen Produkttyp zu erstellen, und Sie haben mehrere Konstruktoren, um eine Summe (von Produkten) zu erstellen.

Angenommen, Sie haben Daten, die Daten entweder als XML oder als JSON ausgeben, und es wird ein Konfigurationsdatensatz benötigt. Die Konfigurationseinstellungen für XML und JSON sind jedoch völlig unterschiedlich. Sie könnten also so etwas tun:

data Config = XML_Config {...} | JSON_Config {...}

(Natürlich mit einigen geeigneten Feldern.) Solche Dinge können Sie in normalen Programmiersprachen nicht machen, weshalb die meisten Leute nicht daran gewöhnt sind.

MathematicalOrchid
quelle
4
großartig! nur eine Sache: "Sie können ... in fast jeder Sprache konstruiert werden", sagt Wikipedia . :) In zB C / ++ ist das unions, mit einer Tag-Disziplin. :)
Will Ness
5
Ja, aber jedes Mal, wenn ich es erwähne union, sehen mich die Leute an wie "Wer zum Teufel benutzt das jemals ?" ;-)
MathematicalOrchid
1
Ich habe unionin meiner C-Karriere viel verwendet gesehen . Bitte lassen Sie es nicht unnötig klingen, da dies nicht der Fall ist.
Truthadjustr
26

Beginnen Sie mit dem einfachsten Fall:

data Color = Blue | Green | Red

Dies definiert einen „Typkonstruktor“ , Colordie keine Argumente haben - und es hat drei „Datenbauer“, Blue, Greenund Red. Keiner der Datenkonstruktoren akzeptiert Argumente. Dies bedeutet , dass es drei vom Typ Color: Blue, GreenundRed .

Ein Datenkonstruktor wird verwendet, wenn Sie einen Wert erstellen müssen. Mögen:

myFavoriteColor :: Color
myFavoriteColor = Green

Erstellt einen Wert myFavoriteColormit dem GreenDatenkonstruktor - und myFavoriteColorist vom Typ, Colorda dies der vom Datenkonstruktor erzeugte Wertetyp ist.

Ein Typkonstruktor wird verwendet, wenn Sie einen Typ erstellen müssen . Dies ist normalerweise beim Schreiben von Unterschriften der Fall:

isFavoriteColor :: Color -> Bool

In diesem Fall rufen Sie den ColorTypkonstruktor auf (der keine Argumente akzeptiert).

Immer noch bei mir?

Stellen Sie sich nun vor, Sie wollten nicht nur Rot / Grün / Blau-Werte erstellen, sondern auch eine "Intensität" angeben. Ein Wert zwischen 0 und 256. Sie können dies tun, indem Sie jedem Datenkonstruktor ein Argument hinzufügen, sodass Sie am Ende Folgendes erhalten:

data Color = Blue Int | Green Int | Red Int

Jetzt nimmt jeder der drei Datenkonstruktoren ein Argument vom Typ Int. Der Typkonstruktor (Color ) akzeptiert immer noch keine Argumente. Da meine Lieblingsfarbe ein dunkles Grün ist, könnte ich schreiben

    myFavoriteColor :: Color
    myFavoriteColor = Green 50

Und wieder nennt es das Green Datenkonstruktor auf und ich erhalte einen Wert vom Typ Color.

Stellen Sie sich vor, Sie möchten nicht vorschreiben, wie Menschen die Intensität einer Farbe ausdrücken. Einige möchten vielleicht einen numerischen Wert, wie wir es gerade getan haben. Andere mögen in Ordnung sein, wenn nur ein Boolescher Wert "hell" oder "nicht so hell" anzeigt. Die Lösung hierfür besteht darin, nicht fest zu codierenInt die Datenkonstruktoren , sondern eine Typvariable zu verwenden:

data Color a = Blue a | Green a | Red a

Jetzt verwendet unser Typkonstruktor ein Argument (einen anderen Typ, den wir gerade aufrufen a!), Und alle Datenkonstruktoren verwenden ein Argument (einen Wert!) Dieses Typs a. Also könntest du haben

myFavoriteColor :: Color Bool
myFavoriteColor = Green False

oder

myFavoriteColor :: Color Int
myFavoriteColor = Green 50

Beachten Sie, wie wir den ColorTypkonstruktor mit einem Argument (einem anderen Typ) aufrufen , um den "effektiven" Typ zu erhalten, der von den Datenkonstruktoren zurückgegeben wird. Dies berührt das Konzept der Arten die Sie bei einer oder zwei Tassen Kaffee lesen möchten.

Nun haben wir herausgefunden, was Datenkonstruktoren und Typkonstruktoren sind und wie Datenkonstruktoren andere Werte als Argumente und Typkonstruktoren andere Typen als Argumente verwenden können. HTH.

Frerich Raabe
quelle
Ich bin mir nicht sicher, ob ich mit Ihrer Vorstellung eines Nulldatenkonstruktors befreundet bin. Ich weiß, dass es in Haskell üblich ist, über Konstanten zu sprechen, aber hat sich das nicht schon einige Male als falsch erwiesen?
kqr
@kqr: Ein Datenkonstruktor kann null sein, ist dann aber keine Funktion mehr. Eine Funktion ist etwas, das ein Argument nimmt und einen Wert ergibt, dh etwas mit ->in der Signatur.
Frerich Raabe
Kann ein Wert auf mehrere Typen verweisen? Oder ist jeder Wert nur einem Typ zugeordnet und das wars?
CMCDragonkai
1
@jrg Es gibt einige Überlappungen, aber dies liegt nicht speziell an Typkonstruktoren, sondern an Typvariablen, z . B. dem ain data Color a = Red a. aist ein Platzhalter für einen beliebigen Typ. Sie können dasselbe jedoch in einfachen Funktionen haben, z. B. nimmt eine Funktion vom Typ (a, b) -> aein Tupel von zwei Werten (von Typen aund b) und liefert den ersten Wert. Es ist eine "generische" Funktion, da sie nicht den Typ der Tupelelemente vorgibt - sie gibt nur an, dass die Funktion einen Wert des gleichen Typs wie das erste Tupelelement liefert.
Frerich Raabe
1
+1 Now, our type constructor takes one argument (another type which we just call a!) and all of the data constructors will take one argument (a value!) of that type a.Das ist sehr hilfreich.
Jonas
5

Wie andere betonten, ist Polymorphismus hier nicht so schrecklich nützlich. Schauen wir uns ein anderes Beispiel an, mit dem Sie wahrscheinlich bereits vertraut sind:

Maybe a = Just a | Nothing

Dieser Typ verfügt über zwei Datenkonstruktoren. Nothingist etwas langweilig, es enthält keine nützlichen Daten. Auf der anderen Seite Justenthält einen Wert von a- welcher Typ aauch immer haben mag. Schreiben wir eine Funktion, die diesen Typ verwendet, z. B. den Kopf einer IntListe zu ermitteln, falls vorhanden (ich hoffe, Sie stimmen zu, dass dies nützlicher ist, als einen Fehler auszulösen):

maybeHead :: [Int] -> Maybe Int
maybeHead [] = Nothing
maybeHead (x:_) = Just x

> maybeHead [1,2,3]    -- Just 1
> maybeHead []         -- None

Also in diesem Fall aist ein Int, aber es würde auch für jeden anderen Typ funktionieren. Tatsächlich können Sie unsere Funktion für jede Art von Liste verwenden (auch ohne die Implementierung zu ändern):

maybeHead :: [t] -> Maybe t
maybeHead [] = Nothing
maybeHead (x:_) = Just x

Auf der anderen Seite können Sie Funktionen schreiben, die nur einen bestimmten Typ von Maybez

doubleMaybe :: Maybe Int -> Maybe Int
doubleMaybe Just x = Just (2*x)
doubleMaybe Nothing= Nothing

Kurz gesagt, mit Polymorphismus geben Sie Ihrem eigenen Typ die Flexibilität, mit Werten verschiedener anderer Typen zu arbeiten.

In Ihrem Beispiel können Sie irgendwann entscheiden, dass dies Stringnicht ausreicht, um das Unternehmen zu identifizieren, aber es muss einen eigenen Typ haben Company(der zusätzliche Daten wie Land, Adresse, Rückkonten usw. enthält). Ihre erste Implementierung von Carmüsste geändert werden, um sie Companyanstelle Stringihres ersten Werts zu verwenden. Ihre zweite Implementierung ist in Ordnung, Sie verwenden sie wie zuvor Car Company String Intund sie würde wie zuvor funktionieren (natürlich müssen Funktionen, die auf Unternehmensdaten zugreifen, geändert werden).

Landei
quelle
Können Sie Typkonstruktoren im Datenkontext einer anderen Datendeklaration verwenden? So etwas wie data Color = Blue ; data Bright = Color? Ich habe es in ghci versucht, und es scheint, dass die Farbe im Typkonstruktor nichts mit dem Farbdatenkonstruktor in der Bright-Definition zu tun hat. Es gibt nur zwei Farbkonstruktoren, einer für Daten und der andere für Typ.
CMCDragonkai
@CMCDragonkai Ich glaube nicht, dass Sie dies tun können, und ich bin mir nicht einmal sicher, was Sie damit erreichen wollen. Sie können einen vorhandenen Typ mit dataoder newtype(z. B. data Bright = Bright Color) "umbrechen" oder verwenden type, um ein Synonym zu definieren (z type Bright = Color. B. ).
Landei
5

Der zweite enthält den Begriff "Polymorphismus".

Das a b ckann von jedem Typ sein. Zum Beispiel akann ein sein [String], bkann sein [Int] und ckann sein[Char] .

Während der erste Typ festgelegt ist: Unternehmen ist ein String, Modell ist ein Stringund Jahr istInt .

Das Auto-Beispiel zeigt möglicherweise nicht die Bedeutung der Verwendung von Polymorphismus. Stellen Sie sich jedoch vor, Ihre Daten sind vom Listentyp. Eine Liste kann enthaltenString, Char, Int ... In diesen Situationen benötigen Sie die zweite Möglichkeit, Ihre Daten zu definieren.

Was den dritten Weg betrifft, denke ich nicht, dass er in den vorherigen Typ passen muss. Dies ist nur eine andere Möglichkeit, Daten in Haskell zu definieren.

Dies ist meine bescheidene Meinung als Anfänger.

Übrigens: Stellen Sie sicher, dass Sie Ihr Gehirn gut trainieren und sich dabei wohl fühlen. Es ist der Schlüssel, um Monad später zu verstehen.

McBear Holden
quelle
1

Es geht um Typen : Im ersten Fall legen Sie die Typen String(für Unternehmen und Modell) und Intfür das Jahr fest. Im zweiten Fall sind Sie allgemeiner. a,, bund ckönnen die gleichen Typen wie im ersten Beispiel sein oder etwas völlig anderes. Beispielsweise kann es nützlich sein, das Jahr als Zeichenfolge anstelle einer Ganzzahl anzugeben. Und wenn Sie möchten, können Sie sogar Ihren ColorTyp verwenden.

Matthias
quelle