18 Kontrollstrukturen
Perse wird imperativer Programmiercode streng sequentiell Befehl für Befehl ausgeführt. Manchmal möchte man von dieser rein sequentiellen Befehlsreihenfolge allerdings abweichen und die Ausführungsreihenfolge von Befehlen flexibler kontrollieren. Prinzipielle Werkzeuge zu diesem Zweck sind konditionale Kontrollstrukturen und iterative Kontrollstrukturen. Konditionale Kontrollstrukturen steuern die Programmausführung, indem sie festlegen, dass bestimmte Befehle nur dann ausgeführt werden, wenn definierte Bedingungen erfüllt sind. R bietet zu diesem Zweck if()
und switch()
Statement. Oft möchte man bestimmte Codeabschnitte auch in Schleifen häufig wiederholen (iterieren). Zu diesem Zweck bietet R for()
, while()
, und repeat()
Schleifen. In diesem Abschnitt stellen wir die grundlegenden konditionalen und iterativen Kontrollstrukturen in R vor und folgen dabei der Darstellung in Cotton (2013), Kapitel 8 und Kapitel 9, sowie Wickham (2019), Kapitel 5.
18.1 Konditionale Kontrollstrukturen
Konditionale Kontrollstrutkuren erlauben die bedingungsabhängige Ausführung von Befehlen.
if-statements
Die grundlegende konditionale Kontrollstruktur ist das if-statement. Die allgemeine Syntax für ein if-statement in R hat folgende Form:
Dabei gilt, dass wenn die Variable Bedingung
den Wert TRUE
hat, also wahr ist, die mit TrueActions
bezeichneten Befehle ausgeführt werden. Ist der Wert der Variable Bedingung
dagegen FALSE
, so werden die Befehle TrueActions
nicht ausgeführt und die Programmausführung wird in der nächsten Zeile nach dem if-statement fortgesetzt. Beispielsweise wird in folgendem Code der Befehl print("x ist größer als 0")
ausgeführt, da x
ja nach vorheriger Zuordnung den Wert 1
hat.
Das if-statement kann durch eine else
Bedingung erweitert werden, um zu spezifieren, welche Befehle in dem Fall ausgeführt werden sollen, dass die Variable Bedingung
den Wert FALSE
hat.
Hier werden die Befehle TrueActions
ausgeführt, wenn die Variable Bedingung
den Wert TRUE
hat und es werden die Befehle FalseActions
ausgeführt, wenn die Variable Bedingung
den Wert FALSE
hat. Beispielsweise wird in folgendem Code der Befehl print("y ist nicht größer als 0")
ausgeführt, da die Bedingung y > 0
nicht zutrifft.
[1] "y ist nicht größer als 0"
R umfasst die in Tabelle 18.1 aufgeführten Relationsoperatoren zur Prüfung binärer logischer Bedingungen.
Relationsoperator | Bedeutung |
---|---|
== |
Gleich |
!= |
Ungleich |
< , > |
Kleiner, Größer |
<= , >= |
Kleiner gleich, Größer gleich |
& |
UND |
| |
ODER |
Dabei werden die Relationsoperatoren <
, <=
, >
, >=
zumeist nur auf numerische Werte angewendet. Die Gleichheitsoperatoren ==
,!=
können auf beliebige Datenstrukturen angewendet werden, um deren Gleichheit zu überprüfen. Die logischen Verknüpfungen &
und |
werden zumeist auf logische Werte angewendet. Die Funktion xor()
schließlich implementiert das exklusive ODER. Es können beispielsweise folgende Tests auf Gleicheit und Relation durchgeführt werden
# Variablendefinition
x = 1
y = 2
# Test auf Gleichheit
if(x == y){
print("x ist gleich y")
} else {
print("x ist ungleich y")
}
[1] "x ist ungleich y"
# Test auf Ungleichheit
if(x < y){
print("x ist kleiner als y")
} else {
print("x ist gfößer oder gleich y")
}
[1] "x ist kleiner als y"
Die if-statement Tests können durch logische Bedingungen erweitert werden, wie folgendes Beispiel demonstrieren soll:
# Variablendefinition
x = 2
y = 2
# logisches UND/ODER
if(x == y | x < y){
print("x ist kleiner oder gleich y")
} else {
print("x ist größer als y")
}
[1] "x ist kleiner oder gleich y"
# logisches UND
if(x > y & x != y){
print("x ist größer als y")
} else {
print("x ist kleiner oder gleich y")
}
[1] "x ist kleiner oder gleich y"
Bei if-Statements ist es entscheidend, dass die Bedingung einem einzigen logischen Wert entspricht. Folgende Bedingungen sind beispielsweise fehlerhaft:
switch-statements
Eine weitere konditionale Kontrollstruktur ist das switch-statement. Switch-statements sind dadurch motiviert, dass kombinierte if-else-statements werden leicht unübersichtlich werden können, wie folgendes Beispiel zeigt:
x = 2
if (x == 1){
print("Aktion 1")
} else if(x == 2){
print("Aktion 2")
} else if(x == 3){
print("Aktion 3")
} else if(x == 4){
print("Aktion 4")
}
[1] "Aktion 2"
Hat man also einen Fall vorliegen, in dem in Abhängigkeit des Wertes einer Variable eine von vielen Aktionen ausgeführt werden soll, so kann sich ein switch-statement zur Verbesserung der Codelesbarkeit anbieten. R implementiert switch-statements in der switch()
Funktion, die in Abhängigkeit vom Datentyp ihrer Variable leicht unterschiedlich funktioniert. Ist die switch Variable vom Typ Integer, so wird die \(i\)te Aktion ausgeführt, wie folgendes Beispiel demonstriert:
x = 2 # switch Variable
switch(x, # x = 2
print("Aktion 1"), # 1. Aktion
print("Aktion 2"), # 2. Aktion
print("Aktion 3"), # 3. Aktion
print("Aktion 4")) # 4. Aktion
[1] "Aktion 2"
Ist die switch-Variable dagegen vom Typ character, so wird die Aktion mit dem entsprechenden Namen ausgeführt:
x = "a" # switch Variable
switch(x, # x = 2
a = print("Aktion a"), # a Aktion
b = print("Aktion b"), # b Aktion
c = print("Aktion c"), # c Aktion
d = print("Aktion d")) # d Aktion
[1] "Aktion a"
Generell lässt sich zu if- und switch-statements festhalten, dass sich jedes if-statemen säquivalent als switch-statement formulieren lässt und andersherum. If-statements sind dabei in der Anwendung sicherlich häufiger anzutreffen als switch-statements. Generell macht es für das menschliche Codeverständnis Sinn, if-else-statements nicht zu komplex zu formulieren. Ist dies allerdings nötig, so können komplexe if-else-statements durch switch-statements vereinfacht werden.
18.2 Iterative Kontrollstrukturen
Iterative Kontrollstrukturen dienen dazu, bestimmte Befehle wiederholt auszuführen. Die grundlegende iterative Kontrollstruktur in R ist die for
-Schleife. Diese hat die generelle Form
Dabei wird der Befehl perform_action
wird für jedes item
in vector
einmal ausgeführt und der Wert von item
jeweils aktualisiert. Folgendes Beispiel soll dies verdeutlichen:
Der Vektor besteht hier aus 1:3 = [1,2,3]
, der ausgeführte Befehl ist print(i)
und die items
sind die Einträge des Vektors. Allerdings muss der Befehl innerhalb der for
-Schleife nicht zwangsläufig auf die items
bezug nehmen, wie folgendes Beispiel zeigt:
Hier wird dreimal der konstante Wert a
ausgegeben. Typischerweise wird innerhalb einer for
-Schleife etwas erzeugt und gespeichert. Dabei sollte generell die entsprechende Speicherstruktur vorallokiert werden, da dann der entsprechende Platz im Arbeitsspeicher schon bereitgestellt ist und nicht während der Ausführung der for
-Schleife bereitgestellt werden muss. Der Effizienzgewinn dieser Strategie mag in den meisten Fällen vernachlässigbar sein, allerdings erlaubt dieses Vorgehen auch insbesondere, Fehler leichter zu detektieren, wenn es beispielsweise Dimensionserwatungsunterschiede oder fehlende Einträge gibt, die dann schon als NaN
alloziert sind. Folgendes Beispiel demonstriert das iterative Auffüllen eines Vektors mit Mittelwerten.
ns = 3 # Anzahl an Simulationen
X_bar = rep(NaN, ns) # Speicherstruktur
# Simulationsiterationen
for(i in 1:ns){ # Iterationsindices sind typischerweise i,j,k
X = rnorm(12) # Realisierung von 12 Z-Variablen
X_bar[i] = mean(X) # Mittelwert der i-ten Realisierung
print(X_bar) # Anzeige zur Demonstration
}
[1] -0.04737814 NaN NaN
[1] -0.04737814 0.06236309 NaN
[1] -0.04737814 0.06236309 0.08232465
Um linear entlang eines Vektors mit variablen Einträgen zu iterieren, bietet sich die Funktion seq_along()
an. Diese erzeugt die linearen Indizes 1
,2
, 3
, … bis zur Länge des betreffenden Vektors, wie folgendes Beispiel demonstriert:
mu = c(0,5,50) # Drei Erwartungswertparameter
X_bar = rep(NaN, ns) # Speicherstruktur
# Simulationsiterationen
for(i in seq_along(mu)){ # Iterationsindices sind typischerweise i,j,k
X = rnorm(12, mu[i], 1) # Realisierung von 12 N(\mu,1) Varialben
X_bar[i] = mean(X) # Mittelwert der i-ten Realisierung
print(X_bar) # Anzeige zur Demonstration
}
[1] -0.3160148 NaN NaN
[1] -0.3160148 5.0948158 NaN
[1] -0.3160148 5.0948158 50.2098457
for
-Schleifen können auch geschachtelt werden, also innerhalb von for
-Schleifen weitere for
-Schleifen ausgeführt werden. Dies mag etwas gewöhnungsbedürftig sein. Entscheidend ist dabei, dass man sich klarmacht, dass für jede Iteration der äußeren Schleife alle Iterationen der inneren Schleife durchlaufen werden. Es ist oft hilfreich, sich die generelle Logik einer geschachtelten Schleifenstruktur zunächst mithilfe von print()
-statements klarzumachen, wie folgendes Beispiel demonstriert:
n_i = 2 # Anzahl der Iterationen äußere Schleife
n_j = 3 # Anzahl der Iterationen innere Schleife
for(i in 1:n_i){ # Äußere Schleife
for (j in 1:n_j){ # Innere Schleife
print(c(i,j)) # Anzeigen der Iternationsindices [i,j]
}
}
[1] 1 1
[1] 1 2
[1] 1 3
[1] 2 1
[1] 2 2
[1] 2 3
Folgendes Beispiel etwa erzeugt eine 4 x 5 Matrix, der Elemente den jeweiligen Spaltenindizes entsprechen. Dabei bedient die innere for
-Schleife mit der Iteratorvariable j
die Spaltenindizes und die äußere for
-Schleife mit der Iteratorvariable i
bedient die Zeilenindizes:
[,1] [,2] [,3] [,4] [,5]
[1,] 1 2 3 4 5
[2,] 1 2 3 4 5
[3,] 1 2 3 4 5
[4,] 1 2 3 4 5
Analog erzeugt folgendes Beispiel eine 4 x 5 Matrix, der Elemente den jeweiligen Spaltenindizes entsprechen. Dabei bedient die innere for
-Schleife mit der Iteratorvariable j
wiederrum die Spaltenindizes und die äußere for
-Schleife mit der Iteratorvariable i
bedient die Zeilenindizes:
[,1] [,2] [,3] [,4] [,5]
[1,] 1 1 1 1 1
[2,] 2 2 2 2 2
[3,] 3 3 3 3 3
[4,] 4 4 4 4 4
In folgendem Beispiel schließlich wird mithilfe zweier geschachtelter for
-Schleifen und eines if
-statements eine 4 x 5 Matrix, deren Elemente gleich den Spaltenindizes sind, wenn der jeweilige Spaltenindex größer oder gleich dem Zeilenindex ist, und deren Elemente ansonsten Null sind erzeugt:
[,1] [,2] [,3] [,4] [,5]
[1,] 1 2 3 4 5
[2,] 0 2 3 4 5
[3,] 0 0 3 4 5
[4,] 0 0 0 4 5