3. Zusammengesetzte Datentypen
3.5 Typenvariable
Im letzten Kapitel haben wir einen Datentyp
DateOrWrongFormat definiert:
type DateOrWrongFormat= WrongFormat| CorrectFormat Date
Sie werden beim Programmieren, besonders in der Praxis,
ständig mit Fehlerszenarios konfrontiert sein.
Eine Datei kann vielleicht nicht geöffnet werden;
ein Http-Request wird womöglich nicht beantwortet; der User
gibt negative Zahlen ein, wo keine erlaubt sind, und so weiter.
Nehmen wir zum Beispiel einen Datentyp Circle:
type alias Circle ={ center : Vector, radius : Float }
Vielleicht wollen wir erzwingen, dass Radius immer
positiv ist (oder zumindest nicht negativ). Wir können,
analog zum Datum, einen opaken Datentyp defineren,
einen CirlceOrWrongFormat-Typ und eine Creator-Funktion.
Also
type Circle= TheCircle Vector Floattype CircleOrWrongFormat= Correct Circle| WrongFormatcreateCircle : Vector -> Float -> CircleOrWrongFormatcreateCircle center radius =if radius < 0 thenWrongFormatelseCorrect (TheCircle center radius)
Nebenbemerkung: Konstruktoren sind Funktionen
Am Rande: in Zeile 35 haben wir zwei ineinander verschachtelte
Konstruktoren: TheCircle nimmt
einen Vector und einen Float
und macht daraus ein Cirlce-Objekt;
Correct nimmt einen Circle
und macht daraus einen CircleOrWrongFormat.
Damit klar ist, welches Argument wozu gehört, brauchen wir Klammern.
Wenn wir die lammern wegließen, also
Correct TheCircle center radius
dann würde Elm denken, der Konstruktor
Correct hat drei Payloads:
TheCircle, center und raidius,
was natürlich Unsinn ist.
Konstruktor-Aufrufe schauen also syntaktisch genauso aus wie
Funktionsaufrufe. Semantisch sind Sie es auch:
Betrachten wir beispielsweise den Typ von
TheCirlce:
TheCirlce<function> : Vector -> Float -> Circle
Es ist also tatsächlich in Syntax und Seminatik eine Funktion!
Allerdings eine Funktion ohne Körper; wenn Sie sich
"echte" Funktionen
(wie f x = x^2) als Maschinen vorstellen,
wo man oben was reinfüllt, das dann verarbeitet wird
und unten wieder rauskommt, dann sind Konstruktoren eher
Verpackungsautomaten; TheCircle tut nichts wirklich,
es verpackt einfach seine drei Argumente und schreibt
TheCircle auf die Verpackung.
Allgemeine CorrectOrWrongFormat-Typen
Gehen wir zurück zu unseren Typen
DateOrWrongFormat und
CircleOrWrongFormat. Wenn der User nun auch
noch Zahlen über die Tastatur eingeben kann, dann müssen
Sie die erstmal von String nach Int
konvertieren, also "13" : String zu 13 : Int.
Was, wenn der User etwas eingibt, das keine Zahl ist?
Hierfür bräuchten wir wiederum einen Typ
IntOrWrongFormat. Und so weiter, für jeden Datentyp.
All diese Datentypen würden fast gleich aussehen.
Um das alles mit einem Schlag für alle Typen
zu erledigen, gibt es in Elm die Typenvariablen:
module CorrectOrWrongFormat exposing (..)type CorrectOrWrongFormat a= Correct a| WrongFormat
Das a in Zeile 4 ist eine Typenvariable, also
eine Vorlage, die dann im Anwendungsfall durch einen
konkreten Typ ersetzt werden muss. Wir
können jetzt DateOpaque umschreiben:
module DateOpaque exposing (Date, createDate)import CorrectOrWrongFormat exposing (..)type Date= TheDate Int Int IntcreateDate : Int -> Int -> Int -> CorrectOrWrongFormat DatecreateDate day month year =if day < 0 thenWrongFormatelse if day > 31 thenWrongFormatelse if (month <= 0) || (month > 12) thenWrongFormatelseCorrect (TheDate day month year)
und dann so verwenden:
import DateOpaque exposing (..)createDate 32 10 2002WrongFormat : CorrectOrWrongFormat DatecreateDate 31 10 2002Correct (TheDate 31 10 2002) : CorrectOrWrongFormat Date
Die beidenn Objekte, die wir im Repl-Fenster erzeugt haben,
sind beide vom Typ CorrectOrWrongFormat Date;
der Type CorrectOrWrongFormat ist erstmal
ein parametrisierter Typ mit Typenvariable a;
erst in DateOpaque wird a durch
Date ersetzt und somit ein vollwertiger Typ
CorrectOrWrongFormat Date erschaffen.
Typenvariable gibt es auch in Funktionsdefinitionen.
In unserer Anwendung wollen wir jetzt aus dem
CorrectOrWrongFormat Date ein
Date-Objket rauskriegen; zum Beispiel,
wenn in Ihrer App bereits ein Daten gespeichert ist und der
User es durch ein neues ersetzen kann; ist die
Eingabe fehlerhaft, dann soll das alte Datum unverändert bleiben.
Der Code in Ihrer Anwendung könnte so aussehen:
letdateSelectedByUser : CorrectOrWrongFormat DatedateSelectedByUser = createDate userDay userMonth userYearnewSelectedDate : DatenewSelectedDate =case dateSelectedByUser ofCorrect date ->dateWrongFormat ->oldSelectedDatein...
In Zeile 915 bauen wir aus den User-Eingaben ein
neues Datum, oder eher ein CorrectOrWrongFormat-Objekt.
Unsere App will nun eine interne Variable für das Datum
aktualisierne. Dafür deklarieren wir newSelectedDate.
Wenn die Usereingaben das richtige Format haben,
dann wählen wir dieses Datum (Zeile 919); falls das Format
falsch war, belassen wir das newSelectedDate
beim alten oldSelectedDate (Zeile 922). Das stellt
sicher, dass oldSelectedDate immer ein
korrektes Format hat. In der Praxis müsste die App
in Zeile 922 irgendwie sicherstellen, dass der User
sinnvolles Feedback bekommt. Da dieser Fall
recht oft vorkommt, schreiben wir uns
eine Funktion
getValueIfCorrectAndAlternativeOfWrongFormat : CorrectOrWrongFormat -> a -> agetValueIfCorrectAndAlternativeOfWrongFormat correctOrWrongData alternative =case CorrectOrWrongFormat ofCorrect something ->somethingWrongFormat ->alternative
So wandeln wir also ein Vielleicht-a in ein a um, müssen aber eine Alternative mit angeben für den Fall, dass es sich doch nicht um ein a handelt.
Das Modul Maybe
Sie müssen das alles aber gar nicht selbst
implementieren. Alles schon geschehen, in dem
Modul Maybe. Das sieht
ungefähr so aus:
module Maybe exposing (..)type Maybe a= Just a| NothingwithDefault : a -> Maybe a -> awithDefault alternative maybeA =case maybeA ofJust something ->somethingNothing ->alternative
Das Modul Maybe definiert noch
weitere Funktionen, aber im Moment
geht es uns um die obigen beiden.
oldValue : IntoldValue = 4userInput : StringuserInput = "13"String.toInt userInputJust 13 : Maybe IntnewValue : IntnewValue = Maybe.withDefault oldValue (String.toInt userInput)13 : IntnewValue = Maybe.withDefault oldValue (String.toInt " 104")4 : Int
Typenvariable erlauben uns also,
mit einer Typendefinition im Prinzip
unendlich viele Typen zu erschaffen. Allerdings: ganz
so neu ist das uns nicht. Wir haben ja bei
Tupeln bereits gesehen, dass sich beliebig viele Datentypen
damit realisieren lassen, also
(Int, Int) und (Float, Float)
und ((Float, Foat), String) und beliebigt verschachtelt.
In der Tat ist die (x,y)-Schreibweise für Tupel
in Elm nur Syntax-Zucker. Das heißt, es bietet uns beim Programmieren
eine vereinfachte Syntax an, ohne wirklich etwas Neues einzuführen. Denn
in der Tat: gäbe es Tupel in Elm nicht von Haus aus, so könnten wir uns
die ja definieren:
module MyTuple exposing (..)type MyTuple a b= MyTuple a bfirst : MyTuple a b -> afirst tuple =case tuple ofMyTuple x y ->xsecond : MyTuple a b -> bsecond tuple =case tuple ofMyTuple x y ->y
und das dann so verwenden:
import MyTuple exposing (..)point2 = MyTuple 0.2 0.5MyTuple.first point10.2 : Float
und natürlich beliebig verschachtelt:
circle = MyTuple point1 4.1MyTuple (MyTuple 0.2 0.5) 4.1 : MyTuple (MyTuple Float Float) Float