4 Control flow

4.1 Choices

Q1: What type of vector does each of the following calls to ifelse() return?

ifelse(TRUE, 1, "no")
ifelse(FALSE, 1, "no")
ifelse(NA, 1, "no")

Read the documentation and write down the rules in your own words.

A: The arguments of ifelse() are named test, yes and no. In general, ifelse() returns the entry for yes when test is TRUE, the entry for no when test is FALSE and NA when test is NA. Therefore, the expressions above return vectors of type double (1), character ("no") and logical (NA).

To be a little more precise, we will cite the part of the documentation on the return value of ifelse():

A vector of the same length and attributes (including dimensions and “class”) as test and data values from the values of yes or no. The mode of the answer will be coerced from logical to accommodate first any values taken from yes and then any values taken from no.

This is surprising because it uses the type of test. In practice this means, that test is first converted to logical and if the result is neither TRUE nor FALSE, simply as.logical(test) is returned.

ifelse(logical(), 1, "no")
#> logical(0)
ifelse(NaN, 1, "no")
#> [1] NA
ifelse(NA_character_, 1, "no")
#> [1] NA
ifelse("a", 1, "no")
#> [1] NA
ifelse("true", 1, "no")
#> [1] 1

Q2: Why does the following code work?

x <- 1:10
if (length(x)) "not empty" else "empty"
#> [1] "not empty"

x <- numeric()
if (length(x)) "not empty" else "empty"
#> [1] "empty"

A: if() expects a logical condition, but also accepts a numeric vector where 0 is treated as FALSE and all other numbers are treated as TRUE. Numerical missing values (including NaN) lead to an error in the same way that a logical missing, NA, does.

4.2 Loops

Q1: Why does this code succeed without errors or warnings?

x <- numeric()
out <- vector("list", length(x))
for (i in 1:length(x)) {
  out[i] <- x[i] ^ 2
}
out

A: This loop is a delicate issue, and we have to consider a few points to explain why it is evaluated without raising any errors or warnings.

The beginning of this code smell is the statement 1:length(x) which creates the index of the for loop. As x has length 0 1:length(x) counts down from 1 to 0. This issue is typically avoided via usage of seq_along(x) or similar helpers which would just generate integer(0) in this case.

As we use [<- and [ for indexing 0-length vectors at their first and zeroth position, we need to be aware of their subsetting behaviour for out-of-bounds and zero indices.

During the first iteration x[1] will generate an NA (out-of-bounds indexing for atomics). The resulting NA (from squaring) will be assigned to the empty length-1 list out[1] (out-of-bounds indexing for lists).

In the next iteration, x[0] will return numeric(0) (zero indexing for atomics). Again, squaring doesn’t change the value and numeric(0) is assigned to out[0] (zero indexing for lists). Assigning a 0-length vector to a 0-length subset works but doesn’t change the object.

Overall, the code works, because each step includes valid R operations (even though the result may not be what the user intended).

Q2: When the following code is evaluated, what can you say about the vector being iterated?

xs <- c(1, 2, 3)
for (x in xs) {
  xs <- c(xs, x * 2)
}
xs
#> [1] 1 2 3 2 4 6

A: In this loop x takes on the values of the initial xs (1, 2 and 3), indicating that it is evaluated just once in the beginning of the loop, not after each iteration. (Otherwise, we would run into an infinite loop.)

Q3: What does the following code tell you about when the index is updated?

for (i in 1:3) {
  i <- i * 2
  print(i) 
}
#> [1] 2
#> [1] 4
#> [1] 6

A: In a for loop the index is updated in the beginning of each iteration. Therefore, reassigning the index symbol during one iteration doesn’t affect the following iterations. (Again, we would otherwise run into an infinite loop.)