3. Zusammengesetzte Datentypen
3.1 Daten bündeln mit Records
Erinnern Sie sich an unseren Versuch aus dem letzten Teilkapitel, Kalenderdaten in als Tripel
darzustellen, also beispielsweise (3, 10, 2023)
. Das Problem ist, dass die
einzelnen Positionen (erste, zweite, dritte) nicht benannt sind und andere Entwickler
nicht wissen, ob Sie die Daten im Format (day, month, year)
oder
(month, day, year)
oder (year, month, day)
repräsentieren. Besser geht es mit Records:
firstDayOfLectures = {day = 4, month = 10, year = 2023}
Jetzt kann es keine Verwechslungen mehr geben. Was wir gerade getan haben, ist,
dem Bezeichner firstDayOfLectures
einen
Wert mit einem Record-Typ zugewiesen zu haben.
Records sind im Prinzip nichts wirklich Anderes als Tupel, nur dass wir
für die einzelnen Einträge selbst Namen vergeben können, statt sie
als erste Koordinate und zweite Koordinate zu bezeichnen.
Öffnen Sie mit Ihrem Code-Editor eine neue Datei und speichern Sie sie als
PP/elm/src/Date.elm
ab. Kopieren Sie folgenden Code hinein:
module Date exposing (..)
type alias Date =
{ day : Int
, month : Int
, year : Int
}
wiedervereinigung : Date
wiedervereinigung =
{ day = 3, month = 10, year = 1990 }
Die Zeilen 4-8 zeigen Ihnen, wie wir einen neuen Record-Datentyp definieren.
Der Name für den neuen Typ, hier Code
, muss zwingend mit einem
Großbuchstaben beginnen. Die Konvention, dass Typen mit Großbuchstaben und
Bezeichner mit Kleinbuchstaben beginnen, gilt auch in anderen Sprachen wie Java.
In Elm ist es aber Teil der Syntax, d.h. Ihr Programm wird nicht starten, wenn Sie sich
nicht an diese Konvention halten.
In Zeile 11-13 konstruieren wir einen Ausdruck vom Typ Date
, indem wir allen
Einträgen Werte zuweisen (hier muss jeder Wert ein Int
sein).
Date
die Einträge
day
, month
und year
heißen Felder
(fields).
Der Type Date
hat also drei Felder, die alle den Typ Int
haben.
Jetzt wissen Sie, wie man ein Record-Objekt konstruiert (Zeile 11-13). Als nächstes lernen Sie, wie man auf die einzelnen Felder zugreift. Öffnen Sie Elm im Repl-Modus:
H:\PP\elm\> elm repl
import Date exposing (..)
wiedervereinigung.month
10 : Int
Auf die einzelnen Felder greifen Sie also genauso zu wie in Java. Wenn Sie
aus einem Record-Objekt ein neues bauen wollen, dass sich in einigen wenigen Feldern
unterscheidet, also beispielsweise das Feld year
auf 2023
setzen,
dann geht das so:
einheitstag2023 = {wiedervereinigung | year = 2023}
Alternativ können Sie natürlich auch
einheitstag2023 = {year = 2023, month = wiedervereinigung.month, day = wiedervereinigung.day}
schreiben, also jedes Feld "per Hand" kopieren.
{ recordObject | fieldName = newValue}
?
Klar, es ist mehr Schreibarbeit. Es gibt aber noch einen gewichtigeren Nachteil.
tomorrow : Date -> Date
, die das Folgedatum ausrechnet.
Achtung: Schreiben Sie nicht alles in eine Datei, sondern
delegieren Sie Unteraufgaben an Helferfunktionen (Beispiel: Sie werden mit
Sicherheit etwas wie isLeapYear : Int -> Bool
brauchen).
compareDate : Date -> Date -> Int
, die zwei Daten vergleicht. Der AufrufcompareDate date1 date2
soll -1 zurückgeben, wenndate1
vordate2
kommt, 1 wenn danach, und 0, wenn sie gleich sind.yesterday : Date -> Date
addDays : Int -> Date -> Date
. Der AufrufaddDays n date
soll das Datum berechnen, das vondate
ausn
Tage in der Zukunft liegt.Tip: Hier brauchen Sie Rekursion. Was machen Sie, wenn \(n\) negativ ist?
-
dateDifference : Date -> Date -> Int
.
Geometrie
Für Punkte im zweidimensionalen Raum, gerichtete Geraden und Dreiecke
legen wir nun Record-Typen an. Legen Sie mit Ihrem Code-Editor eine neue
Datei an und speichern Sie sie unter
PP/elm/src/Geometry.elm
. Kopieren Sie folgenden Code hinein:
module Geometry exposing (..)
type alias Vector =
{ x : Float
, y : Float
}
type alias DirectedLine =
{ from : Vector
, to : Vector
}
type alias Triangle =
{ a : Vector, b : Vector, c : Vector }
All dies hätten wir auch mit Tupeln geschafft. Allerdings ist es nun klarer, was womit gemeint ist (und später wird der Unterschied noch klarer). Ich zeige Ihnen nun, wie Sie Records anlegen und auf die einzelnen Einträge zugreifen:
v : Vector
v = {x = 0.4, y = 0.8} -- so initialisieren Sie einen Record
v.x -- so greifen Sie auf die Einträge zu; wie in Java
0.4 : Float
Als nächstes implementieren wir die Funktion rotateVector
. Dies
haben Sie ja hoffentlich in Kapitel 2.7 bereits
getan, allerdings mit Tupeln und nicht mit Records.
rotateVector : Float -> Vector -> Vector
rotateVector angle v =
let
sinAngle : Float
sinAngle =
sin angle
cosAngle : Float
cosAngle =
cos angle
in
{ x = v.x * cosAngle - v.y * sinAngle, y = v.x * sinAngle + v.y * cosAngle }
leftRightOrOnLine : DirectedLine -> Vector -> Intwobei
leftRightOrOnLine line v
Ihnen 1, -1 oder 0 zurückgibt, je nachdem,
ob der Punkt v
links von, rechts von oder auf der gerichteten Geraden
line
liegt.
insideTriangle : Triangle -> Vector -> Boolso dass
insideTriangle triangle v
zurückgibt, ob
der Punkt v
im Inneren des Dreiecks triangle
liegt.
Circle
bestehend aus einem Mittelpunkt
und einem Radius. Schreiben Sie eine Funktion
insideCircle
, die überprüft, ob ein gegebener Punkt
im inneren eines gegebenen Kreises liegt.
- Definieren Sie den Datentyp analog zu den obigen Beispielen
mit
type alias Circle = ...
-
Legen Sie eine Signatur für
insideCircle
fest. Was sollte der erste Parameter sein? Der Kreis oder der Punkt? Ihre Entscheidung, überlegen Sie sich aber, was besser wäre.
Heterogene Daten
Nur damit die bisherigen Beispiele Sie nicht glauben machen, alle Felder eines Record-Typs
müssten die
gleichen Typen haben (Bei Kalenderdaten waren alle Felder Int
; bei
unseren Vektoren waren sie alle Float
): dies ist sicherlich nicht der Fall.
Sie dürfen gerne heterogene Records erstellen (das ist sogar der Regelfall), wie beispielsweise
type alias Resident = { firstname : String , lastname : String , dateOfBirth : Date , street : String , houseNumber : Int , city : String , zipCode : Int , taxNumber : Int , married : Bool }
Resident
weist mehrere Eigenschaften auf,
die in der Praxis zu Problemen führen können. Welche?
Das offensichtlichste Problem ist, dass houseNumber
ein Int
ist.
Denn wie repräsentieren Sie dann Adressen wie Bahnhofstraße 2a
?
Was tun Sie mit Ihrer Software, wenn die deutsche Post plötzlich beginnt, Buchstaben in
die Postleitzahlen (zipCode
) zu integrieren?
Auf die Frage, ob man Daten als String
oder Int
darstellen sollte,
hat Phil Scovis auf Quora eine
wunderbare
Antwort gegeben:
So, here's a quick and easy test for deciding between ints or strings.
Will you be adding them?
There it is. Try it with phone numbers, zip codes, or ABA routing numbers. Try it with inventory counts, pixel positions, time ticks. See the difference?
As you say in your question, room numbers "are not used mathematically". That's like 99% of the "int" interface, right there. On the other hand, you will be comparing them, and possibly extracting digits (like for a floor number). Those are common string operations.
We think of the data type as "what something is"; but it is just as important to ask "what will it need to do". The second question can give valuable insight on the first.