3. Zusammengesetzte Datentypen
3.3 Heterogene Daten mit Custom Types
Zusätzlich zu Records gibt es in Elm eine weitere Methode, neue Datentypen zu definieren,
die fast noch wichtiger ist (und zu der es kein offensichtliches Gegenstück in Java gibt). Gehen
wir zu unserem Datentyp Resident
zurück, den wir am Ende von
Kapitel 3.1 definiert haben. In stark vereinfachter Form nun:
type alias Resident = { name : String , dateOfBirth : Date , married : Bool }
Das Problem hier? Das Feld married
sollte eher maritalStatus
heißen,
weil der Familienstand
ja nicht nur die Werte ledig und verheiratet annehmen kann, sondern auch noch
verwitwet und geschieden. Ganz oft sind wir beim Programmieren in der
Situation, dass es
für eine gewisse Variable nur endlich viele, von vornherein feststehende Werte geben kann.
Wochentag kann
sieben Werte annehmen. Aggregatszustand nur fest, flüssig und
gasförmig
(im Alltag; in ungewöhnlicheren physikalischen Situationen kommen
noch Plasma, Bose-Einstein-Kondensat etc. hinzu). Wie repräsentieren wir also
den Familienstand in Elm? Ein Weg, den man in Sprachen wie C/C++ oft geht, ist, einfach
Zahlencodes für die verschiedenen Zustände einzuführen:
module Resident exposing (..)
import Date exposing (..)
type alias Resident =
{ name : String
, dateOfBirth : Date
, maritalStatus : Int
}
single : Int
single =
0
married : Int
married =
1
divorced : Int
divorced =
2
widowed : Int
widowed =
3
Wir verwenden also die Zahlen 0 bis 3, um die vier Zustände zu codieren.
Andere Entwickler, die auf unserem Code aufbauen, müssten dann hoch und heilig versprechen,
nie ungültige Werte (4 oder -1) zu verwenden, und am Besten auch nie
1
zu schreiben sondern immer divorced
. Zum Beispiel:
computeTaxRate : Resident -> BunchOfOtherData -> Float
computeTaxRate resident otherData =
if resident.maritalStatus == married then
... some value here
else
... some other value here
Hier werden also verheiratete Personen auf eine Weise besteuert und alle anderen,
egal ob ledig, verwitwet oder geschieden, auf eine zweite Weise.
Nun führt das Parlament einen weiteren Familienstand ein: in Lebenspartnerschaft
(civil union),
und stellt diesen steuerrechtlich verheirateten Leuten gleich.
Ihr Team muss nun die Software aktualisieren und fügt dem Modul Resident
eine
weitere
Zeile hinzu:
civilUnion : Int
civilUnion =
4
Jetzt müssen natürlich auch allen Stellen im Code (nicht nur Ihr Team; alle anderen
auch)
anpassen, in welchen der Familienstand abgefragt wird. Was passiert, wenn
Sie vergessen, computeTaxRate
zu aktualisieren? Ihr Code läuft immer noch;
es kommen keine Fehlermeldungen, dafür werden Personen in Lebenspartnerschaft wie ledige
Personen, und eben nicht wie verheiratete Personen besteuert, da
auch sie im else
-Teil, also in Zeilen 819/820 landen.
Custom Types für endliche Aufzählungen
Anstatt in Lebenspartnerschaft lebende Personen falsch zu besteuern, wollen wir, dass der Elm-Compiler dies bereits als Fehler erkennt und uns mitteilt: Halt! Sie haben hier eine Möglichkeit unberücksichtigt gelassen! Das geht ganz automatisch mit Custom Types.
module Resident exposing (..)
import Date exposing (..)
type alias Resident =
{ name : String
, dateOfBirth : Date
, maritalStatus : MaritalStatus
}
type MaritalStatus
= Single
| Married
| Divorced
| Widowed
Ähnlich wie Bool
, das nur zwei Werte annehmen kann, nämlich True
und
False
,
so ist MaritalStatus
ein Typ, der nur vier Werte annehmen kann:
Single
, Married
, Divorced
, Widowed
. Diese
Werte nennt man Konstanten. Wir haben also einen neune Custom-Typ und vier neue
Konstanten
eingeführt. Konstanten müssen in Elm grundsätzlich mit einem großen Buchstaben beginnen (wie ja
auch
True
und False
). Sie verarbeiten Sie ganz normal wie andere Werte
auch:
a = Married
b = Divorced
a == b
False : Bool
a == Married
True : Bool
Wir können nun also computeTaxRate
wieder mit einer
if/else
-Kette implementieren. Tun Sie das aber nicht!
Wenn Sie es mit einem Custom-Typ zu tun haben, dann verarbeiten Sie die bitte
wenn möglich immer mit case ... of
:
computeTaxRate : Resident -> BunchOfOtherData -> Float
computeTaxRate resident otherData =
case resident.maritalStatus of
Single ->
0.3
Married ->
0.2
Divorced ->
0.3
Widowed ->
0.3
Die 0.2 und 0.3 sind natürlich Fantasiezahlen. Was ist nun der Vorteil gegenüber den Zahlencodes 0, 1, 2, 3?
H:\PP\elm\src\
. Starten Sie ein Repl-Fenster, legen
einen Resident
an und testen die Funktion computeTaxRate
.
Editieren Sie nun die Datei Resident.elm
indem Sie der Typendefinition von
MaritalStatus
die weitere Alternative CivilUnion
hinzufügen.
Rufen Sie computeTaxRate
auf und schauen, was geschieht!
Fehlermeldungen
Sie bekommen wahrscheinlich eine Fehlermeldung, in etwa
This `case` does not have branches for all possibilities:
23|> case resident.maritalStatus of
...
Missing possibilities include:
CivilUnion
Ist das jetzt gut oder schlecht? Ich würde sagen, es ist gut! Der Elm-Compiler
hat den Fehler entdeckt und unser Entwicklungsteam kann es korrigieren. Die Alternative
mit der if/else
-Kette, dass nämlich Personen falsch besteuert werden, wäre ungleich
schlimmer.
Wir haben es hier mit Compilerfehler versus Laufzeitfehler versus
Logikfehlerzu tun:
- Compilerfehler. Der Compiler für die Programmiersprache erkennt
einen Fehler. Vielleicht haben Sie irgendwo ein Komma vergessen (oder in Java ein
Semikolon);
vielleicht stimmen Typen nicht überein.
Compilerfehler in Elm:
import Resident exposing (..)
-- MISSING PATTERNS ---- temp/Resident.elm This `case` does not have branches for all possibilities: 23|> case resident.maritalStatus of
Compilerfehler in Java:
javac Recursion.java
Recursion.java:6: error: incompatible types: int cannot be converted to String
return 0;
...
- Laufzeitfehler. Der Code läuft und rechnet, kommt aber irgendwann
an eine Stelle, an der offensichtlich ein Fehler aufgetreten ist. In Java zum Beispiel
können
ArrayIndexOutOfBoundException
auftreten, wenn Sie ein Array der Länge 8 haben, aber auf Position 12 zugreifen wollen. Oder eineNumberFormatException
, wenn Sie beispielsweise einen String in eine Zahl umwandeln wollen, obwohl der gar keine Zahl darstellt:Laufzeitfehler in Elm:
Elm brüstet sich mit:
No Runtime Exceptions
Elm uses type inference to detect corner cases and give friendly hints. NoRedInk switched to Elm about four years ago, and 300k+ lines later, they still have not had to scramble to fix a confusing runtime exception in production. Learn more.Das stimmt fast, aber nicht ganz: seit ich im Sommer 2022 mit Elm begonnen habe, ist mir ein "richtiger" Laufzeitfehler begegnet. Aber immerhin: ein einziger, während man in Java gefühlt alle fünf Minuten einen kriegt.
Laufzeitfehler in Java:
java Recursion
Exception in thread "main" java.lang.NumberFormatException: For input string: "193O"
...Übrigens: erkennen Sie, was an "193O" falsch ist?
Ein Laufzeitfehler liegt also immer vor, wenn Ihr Programm "abstürzt".
-
Logikfehler. Ein Logikfehler liegt vor, wenn Ihr Programm compiliert und
nicht
abstürzt, aber dennoch ein falsches Ergebnis liefert. Logikfehler können Ihnen in jeder
Programmiersprache
unterlaufen (Sie können immer \(\cos\) mit \(\sin\) verwechseln, daran hindert Sie kein
Compiler und
keine Laufzeitumgebung), auch weil ja nicht immer klar ist, was Sie unter einem richtigen
Ergebnis verstehen.
Wie wir allerdings in unserem
MaritalStatus
-Beispiel gesehen haben, trägt das strenge Typensystem von Elm durchaus dazu bei, Logikfehlern vorzubeugen (hier: Leute nicht inkorrekt zu besteuern).