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 Float
type CircleOrWrongFormat
= Correct Circle
| WrongFormat
createCircle : Vector -> Float -> CircleOrWrongFormat
createCircle center radius =
if radius < 0 then
WrongFormat
else
Correct (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 Int
createDate : Int -> Int -> Int -> CorrectOrWrongFormat Date
createDate day month year =
if day < 0 then
WrongFormat
else if day > 31 then
WrongFormat
else if (month <= 0) || (month > 12) then
WrongFormat
else
Correct (TheDate day month year)
und dann so verwenden:
import DateOpaque exposing (..)
createDate 32 10 2002
WrongFormat : CorrectOrWrongFormat Date
createDate 31 10 2002
Correct (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:
let
dateSelectedByUser : CorrectOrWrongFormat Date
dateSelectedByUser = createDate userDay userMonth userYear
newSelectedDate : Date
newSelectedDate =
case dateSelectedByUser of
Correct date ->
date
WrongFormat ->
oldSelectedDate
in
...
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 -> a
getValueIfCorrectAndAlternativeOfWrongFormat correctOrWrongData alternative =
case CorrectOrWrongFormat of
Correct something ->
something
WrongFormat ->
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
| Nothing
withDefault : a -> Maybe a -> a
withDefault alternative maybeA =
case maybeA of
Just something ->
something
Nothing ->
alternative
Das Modul Maybe
definiert noch
weitere Funktionen, aber im Moment
geht es uns um die obigen beiden.
oldValue : Int
oldValue = 4
userInput : String
userInput = "13"
String.toInt userInput
Just 13 : Maybe Int
newValue : Int
newValue = Maybe.withDefault oldValue (String.toInt userInput)
13 : Int
newValue = 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 b
first : MyTuple a b -> a
first tuple =
case tuple of
MyTuple x y ->
x
second : MyTuple a b -> b
second tuple =
case tuple of
MyTuple x y ->
y
und das dann so verwenden:
import MyTuple exposing (..)
point2 = MyTuple 0.2 0.5
MyTuple.first point1
0.2 : Float
und natürlich beliebig verschachtelt:
circle = MyTuple point1 4.1
MyTuple (MyTuple 0.2 0.5) 4.1 : MyTuple (MyTuple Float Float) Float