18  Control structures

Per se, imperative program code is executed strictly sequentially, command by command. Sometimes, however, one wants to deviate from this purely sequential order of commands and control the execution order of commands more flexibly. Principal tools for this purpose are conditional control structures and iterative control structures. Conditional control structures control program execution by specifying that certain commands are executed only when defined conditions are met. For this purpose, R provides if() and switch() statements. Often, one also wants to repeat (iterate) certain sections of code frequently in loops. For this purpose, R provides for(), while(), and repeat() loops. In this section, we introduce the basic conditional and iterative control structures in R, following the presentation in Cotton (2013), Chapter 8 and Chapter 9, as well as Wickham (2019), Chapter 5.

18.1 Conditional control structures

Conditional control structures allow the condition-dependent execution of commands.

if-statements

The basic conditional control structure is the if-statement. The general syntax for an if-statement in R has the following form:

if (condition){
  true_actions
}

Here, if the variable condition has the value TRUE, that is, if it is true, the commands denoted by true_actions are executed. If the value of the variable condition is FALSE, by contrast, the commands true_actions are not executed, and program execution continues on the next line after the if-statement. For example, in the following code the command print("x is greater than 0") is executed, since x has the value 1 after the preceding assignment.

x = 1
if(x > 0){
  print("x is greater than 0")
}
[1] "x is greater than 0"

The if-statement can be extended by an else condition to specify which commands are to be executed in the case that the variable condition has the value FALSE.

if (condition){
  true_actions
} else {
  false_actions
}

Here, the commands true_actions are executed if the variable condition has the value TRUE, and the commands false_actions are executed if the variable condition has the value FALSE. For example, in the following code the command print("y is not greater than 0") is executed because the condition y > 0 is not met.

y = -1
if(y > 0){
  print("y is greater than 0")
} else{
  print("y is not greater than 0")
}
[1] "y is not greater than 0"

R includes the relation operators listed in Table 18.1 for testing binary logical conditions.

Table 18.1: Relation operators in R
Relation operator Meaning
== Equal
!= Unequal
<, > Smaller, greater
<=, >= Smaller or equal, greater or equal
& AND
| OR

The relation operators <, <=, >, >= are mostly applied only to numerical values. The equality operators ==,!= can be applied to arbitrary data structures to check their equality. The logical links & and | are mostly applied to logical values. Finally, the function xor() implements the exclusive OR. For example, the following tests of equality and relation can be performed:

# variable definition
x = 1
y = 2

# test of equality
if(x == y){
  print("x is equal to y")
} else {
  print("x is unequal to y")
}
[1] "x is unequal to y"
# test of inequality
if(x < y){
  print("x is smaller than y")
} else {
  print("x is greater than or equal to y")
}
[1] "x is smaller than y"

The if-statement tests can be extended by logical conditions, as the following example is intended to demonstrate:

# variable definition
x = 2
y = 2

# logical AND/OR
if(x == y | x < y){
  print("x is smaller than or equal to y")
} else {
  print("x is greater than y")
}
[1] "x is smaller than or equal to y"
# logical AND
if(x > y & x != y){
  print("x is greater than y")
} else {
  print("x is smaller than or equal to y")
}
[1] "x is smaller than or equal to y"

For if-statements, it is crucial that the condition corresponds to a single logical value. The following conditions, for example, are erroneous:

if("x"){
  print(1)
}
Error in `if ("x") ...`:
! argument is not interpretable as logical
if(NA){
  print(1)
}
Error in `if (NA) ...`:
! missing value where TRUE/FALSE needed
if(c(T,F)){
  print(1)
}
Error in `if (c(T, F)) ...`:
! the condition has length > 1

switch-statements

Another conditional control structure is the switch-statement. Switch-statements are motivated by the fact that combined if-else-statements can easily become unclear, as the following example shows:

x = 2
if (x == 1){
  print("action 1")
} else if(x == 2){
  print("action 2")
} else if(x == 3){
  print("action 3")
} else if(x == 4){
  print("action 4")
}
[1] "action 2"

Thus, if one has a case in which one of many actions is to be executed depending on the value of a variable, a switch-statement can be useful for improving code readability. R implements switch-statements in the switch() function, which works slightly differently depending on the data type of its variable. If the switch variable is of type integer, the \(i\)th action is executed, as the following example demonstrates:

x = 2               # switch variable
switch(x,           # x = 2
print("action 1"),  # 1st action
print("action 2"),  # 2nd action
print("action 3"),  # 3rd action
print("action 4"))  # 4th action
[1] "action 2"

If the switch variable is of type character, by contrast, the action with the corresponding name is executed:

x = "a"                 # switch variable
switch(x,               # x = "a"
a = print("action a"),  # a action
b = print("action b"),  # b action
c = print("action c"),  # c action
d = print("action d"))  # d action
[1] "action a"

In general, it can be said of if- and switch-statements that every if-statement can be formulated equivalently as a switch-statement and vice versa. In practice, if-statements are certainly encountered more frequently than switch-statements. In general, for human code comprehension it makes sense not to formulate if-else-statements too complexly. If this is necessary, however, complex if-else-statements can be simplified by switch-statements.

18.2 Iterative control structures

Iterative control structures serve to execute certain commands repeatedly. The basic iterative control structure in R is the for loop. It has the general form

for (item in vector){
  perform_action
}

The command perform_action is executed once for each item in vector, and the value of item is updated each time. The following example is intended to illustrate this:

for (i in 1:3){
  print(i)
}
[1] 1
[1] 2
[1] 3

Here the vector consists of 1:3 = [1,2,3], the command executed is print(i), and the items are the entries of the vector. However, the command within the for loop does not necessarily have to refer to the items, as the following example shows:

for (i in 1:3){
  print('a')
}
[1] "a"
[1] "a"
[1] "a"

Here, the constant value a is output three times. Typically, something is generated and stored within a for loop. In doing so, the corresponding storage structure should generally be preallocated, because the corresponding space in memory is then already provided and does not have to be provided during the execution of the for loop. The efficiency gain of this strategy may be negligible in most cases, but this procedure also makes it easier in particular to detect errors, for example if there are discrepancies in dimensional expectations or missing entries that are then already allocated as NaN. The following example demonstrates the iterative filling of a vector with means.

ns    = 3               # number of simulations
X_bar = rep(NaN, ns)    # storage structure

# simulation iterations
for(i in 1:ns){         # iteration indices are typically i,j,k
  X        = rnorm(12)  # realization of 12 Z variables
  X_bar[i] = mean(X)    # mean of the ith realization
  print(X_bar)          # display for demonstration
}
[1] 0.2388975       NaN       NaN
[1]  0.23889754 -0.06565672         NaN
[1]  0.23889754 -0.06565672  0.24848672

To iterate linearly along a vector with variable entries, the function seq_along() is useful. It generates the linear indices 1,2, 3, … up to the length of the vector in question, as the following example demonstrates:

mu    = c(0,5,50)                 # three expected-value parameters
X_bar = rep(NaN, ns)              # storage structure

# simulation iterations
for(i in seq_along(mu)){          # iteration indices are typically i,j,k
  X        = rnorm(12, mu[i], 1)  # realization of 12 N(mu,1) variables
  X_bar[i] = mean(X)              # mean of the ith realization
  print(X_bar)                    # display for demonstration
}
[1] 0.009096054         NaN         NaN
[1] 0.009096054 4.921538604         NaN
[1]  0.009096054  4.921538604 50.189367556

for loops can also be nested, that is, further for loops can be executed within for loops. This may take some getting used to. The crucial point here is to realize that for each iteration of the outer loop, all iterations of the inner loop are run through. It is often helpful first to clarify the general logic of a nested loop structure with the help of print() statements, as the following example demonstrates:

n_i = 2              # number of iterations of the outer loop
n_j = 3              # number of iterations of the inner loop
for(i in 1:n_i){     # outer loop
  for (j in 1:n_j){  # inner loop
    print(c(i,j))    # display of the iteration indices [i,j]
    }
}
[1] 1 1
[1] 1 2
[1] 1 3
[1] 2 1
[1] 2 2
[1] 2 3

The following example, for instance, generates a 4 x 5 matrix whose elements correspond to the respective column indices. The inner for loop with iterator variable j serves the column indices, and the outer for loop with iterator variable i serves the row indices:

     [,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

Analogously, the following example generates a 4 x 5 matrix whose elements correspond to the respective row indices. The inner for loop with iterator variable j again serves the column indices, and the outer for loop with iterator variable i serves the row indices:

     [,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

Finally, in the following example, two nested for loops and an if-statement are used to generate a 4 x 5 matrix whose elements are equal to the column indices if the respective column index is greater than or equal to the row index, and whose elements are otherwise zero:

     [,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
Cotton, R. (2013). Learning R (First Edition). O’Reilly.
Wickham, H. (2019). Advanced R, Second Edition. CRC Press.