4.3 Repeating Things: Looping

We have looked a bit into the aspect of flow control that pertains to making decisions. Let us now turn to the R-constructs that make the computer repeat actions.

4.3.1 For Loops

The reserved word for is used to make R repeat an action a specified number of times.

We begin with an very simple example:

for ( i in 1:4 ) {
  cat("Hello, Dorothy!\n")
}
## Hello, Dorothy!
## Hello, Dorothy!
## Hello, Dorothy!
## Hello, Dorothy!

Here’s how the loop works. Recall that the vector 1:4 is simply the sequence of integers from 1 to 4:

1:4
## [1] 1 2 3 4

When R sees the code (i in 1:4) it knows that it will have to go four times through the body of the loop.

cat("Hello, Dorothy!\n")

(The body of a loop is what’s contained in the brackets after for(i in 1:4)). At the start of the loop, the index variable i is set to 1. After the body is executed the first time, R sets i to 2, then executes the body again. Then R sets i to 3 and executes the body yet again. Then R sets i to 4, and executes the body for the final time. The result is four lines printed out to the console.

The more you need to repeat a particular pattern, the more it makes sense to write your code with a loop.

The general form of a loop is:

for ( var in seq ) {
  # code involving var
}

var is the index variable, and it can be any permitted name for a variable, and seq can be any vector. As R traverses the loop, the value of the index variable var becomes each element of the vector seq in turn. With every change in the value of var, the code in the brackets is executed.

To iterate is to do a thing again and again. The vector seq is sometimes called an iterable, since it is “iterated over.” It contains the values that the index variable will assume, one by one, as the loop is repeated.

It’s important to realize that the index variable can have any valid name, and the sequence can be any type of vector at all—not just a sequence of consecutive whole numbers beginning with 1. This level of generality permits a for-loop to automate a wide variety of tasks, and for its code to be written in a way that evokes the operations being performed.

For example, here is a loop to print out some greetings;

people <- c("Dorothy", "Tin Man", "Scarecrow", "Lion")
for ( person in people ) {
  cat("Hello, ", person, "!\n", sep = "")
}
## Hello, Dorothy!
## Hello, Tin Man!
## Hello, Scarecrow!
## Hello, Lion!

Or perhaps you want to abbreviate a vector-ful of weekday-names:

weekdays <- c("Saturday", "Monday", "Friday", "Saturday")
for ( day in weekdays ) {
  print(abbrDay(day))
}
## [1] "Sat"
## [1] "Mon"
## [1] "Fri"
## [1] "Sat"

Quite often you will want to store the results of your trips through the loop. Let’s do this for our abbreviated weekdays:

weekdays <- c("Saturday", "Monday", "Friday", "Saturday")
abbrDays <- character(length(weekdays))

We used the character() function to create the character vector abbrDays. We specified that the number of elements in abbrDays shall be the same as the number of elements in weekdays. Right now abbrDays isn’t very interesting, as all of its elements are empty strings:

abbrDays
## [1] "" "" "" ""

We will now write a for-loop to fill it up with abbreviations:

for ( i in 1:length(weekdays) ) {
  abbrDays[i] <- abbrDay(weekdays[i])
}

Now each of the four elements of abbrDays contains the abbreviation for the corresponding element of weekdays:

abbrDays
## [1] "Sat" "Mon" "Fri" "Sat"

You will often have reason to set up an empty vector of a definite length and use a loop to store information in it. The general format looks like this:

results <- numeric(someLength) # empty numerical vector

for ( i in 1:someLength ) {
  # computations involving i, and then:
  results[i] <- some_result_depending_on_i
}

# then do something with results, such as:
print(results)

4.3.2 For-Loop Caution

You might need to exercise some caution in your choice name for the index variable. If you are already using it as the name for a variable in the same environment, then you will overwrite that variable, as demonstrated by the following code:

day <- "Thursday"
cat("Today is ", day, ".\n", sep = "")
## Today is Thursday.
weekdays <- c("Saturday", "Monday", "Friday", "Saturday")
abbrDays <- character(length(weekdays))
for ( day in weekdays ) {
  print(day)
}
## [1] "Saturday"
## [1] "Monday"
## [1] "Friday"
## [1] "Saturday"
cat("Today is ", day, ".\n", sep = "")
## Today is Saturday.

Of course, that won’t happen if the for-loop is inside of a function and your variable is outside of it.

day <- "Thursday"
cat("Today is ", day, ".\n", sep = "")
## Today is Thursday.
weekdays <- c("Saturday", "Monday", "Friday", "Saturday")
listDays <- function(days) {
  for ( day in days )    {
    print(day)
  }
}
listDays(weekdays)
## [1] "Saturday"
## [1] "Monday"
## [1] "Friday"
## [1] "Saturday"
cat("Today is still ", day, ".\n", sep = "")
## Today is still Thursday.

4.3.3 Breaking Out of a Loop

Sometimes you finish the task at hand before you are done with the loop. If this is a possibility for you then you may arrange to break out of the loop with the break reserved-word.

Suppose for example that you want a function that searches through a vector for a given element, updating the user along the way as to the progress of the search. You can try something like this:

# function to find index of element in vector.
# returns -1 if elem is not in vector
verboseSearch <- function(elem, vec) {
  
  # The following logical keeps track of whether
  # we have found the element.
  # We have not yet begun the search so start it
  # at FALSE.
  found <- FALSE
  
  # validate input:
  if ( length(vec) == 0 ) {
    cat("The vector empty.  No way does it contain ", 
        elem, ".", sep = "")
    return(-1)
  }
  
  # check the elements of vector:
  for ( i in 1: length(vec) ) {
    if ( vec[i] == elem ) {
      # record that we found the element:
      found <- TRUE
      break
    } else {
      # report no match at this index:
      cat("Checked index ", i, 
          " in the vector.  No match there ...\n", sep = "")
    }
  }
  
  if ( found ) {
    # report success:
    cat("Found ", elem, " at index ", i, ".\n", sep = "")
    return(i)
  } else {
    # report failure:
    cat(elem, " is not in the vector.\n", sep = "")
    return(-1)
  }
}

Let’s see our function in action:

people <- c("Dorothy", "Tin Man", "Scarecrow", "Lion")
scarecrowPlace <- verboseSearch("Scarecrow", people)
## Checked index 1 in the vector.  No match there ...
## Checked index 2 in the vector.  No match there ...
## Found Scarecrow at index 3.

In the code for verboseSearch() you will notice that there is an initial check on the length of the vector. This is actually important. If a user were to enter an empty vector, then its length would be 0. Then in the loop the sequence would be 1:0, which is the vector with elements 1 and 0. But look at what happens when you ask for any element of a zero-length vector:

emptyVec <- character(0)
emptyVec[1]  # You get NA
## [1] NA

Then check out what happens if you compare an NA to a string:

NA == "Scarecrow"
## [1] NA

Now look at what happens in an if statement where the condition is NA:

if ( NA ) {
  cat("We are in the bracket!\n")
}
## Error in if (NA) { : missing value where TRUE/FALSE needed

Checking that the input vector has positive length is an another example of validating input. Remember: when you write functions for other people to use it can be helpful to have the function validate its input instead of allowing R to throw obscure error messages at the user.

4.3.4 Solving the Empty-Vector Problem in for-Loops with seq_along()

In the previous section we considered a possible problem with for-loops of the following form:

for ( i in 1:length(vec) ) {
  ## do something ...
}

In the above loop vec could be thought of as a “loop-defining” vector: its length determines the sequence of values 1, 2, 3 … for the index i. This sequence of values is supposed to end at the length of vec.

The problem is that if vec happens to be an empty vector then we probably don’t want to enter the loop at all. However, the length of an empty vector is 0, and so the vector 1:length(vec) actually works out to be a vector with two elements:

c(1,0)

Hence R will go through the loop twice: once when i is 1, and again when i is 0. Depending on what the loop does, very unexpected results could be produced.

In the previous section we dealt with the problem by writing an if-statement that provides the proper response when vec is empty. In many circumstances however, all we need to do is to make sure that the loop is skipped when the vector is empty.

A handy way to ensure skipping is to use the function seq_along() . Given any non-empty vector, seq_along() produces a sequence-vector that begins with 1 and ends with the length of the vector, thus:

vec <- c("a", "d", "f")
seq_along(vec)
## [1] 1 2 3

On the other hand, if the vector is empty, then seq_long() returns an empty numeric vector:

vec <- character()
seq_along(vec)
## integer(0)

Now consider the loop inside the following function:

loopy <- function(vec) {
  for ( i in seq_along(vec) ) {
    cat("This time i is ", i, ".\n", sep = "")
  }
}

Given a non-empty vector, it goes through the loop a number of times equal to the length of the vector:

loopy(c("a", "d", "f"))
## This time i is 1.
## This time i is 2.
## This time i is 3.

On the other hand, when given an empty vector the function does not enter the loop at all:

loopy(character())  # no output to console!

When you are writing a program that is complex enough that you don’t know whether the loop-defining vector might be empty, it is good practice to use seq_along() as a safeguard.

4.3.5 Skipping Ahead in a Loop

Depending on what happens within a loop, you might sometimes wish to skip the remaining code within the loop and proceed to the next iteration. R provides the reserved-word next for this purpose. Here is a simple example:

vec <- c("a","e", "e", "i", "o", "u", "e", "z")
# shout ahoy when you see the specified element
verboseAhoy <- function(elem, vec) {
  if (length(vec) > 0) {
    for ( i in 1: length(vec) ) {
      if ( vec[i] != elem) next
      cat("Ahoy! ", elem, " at index ", i, "!\n", sep = "")
    }
  }
  
}
verboseAhoy("e", vec)
## Ahoy! e at index 2!
## Ahoy! e at index 3!
## Ahoy! e at index 7!

When the vec[i] !== elem condition is true, R immediately skips the rest of the loop, increments the value of the index variable i, and runs through the loop again.

You can always accomplish the skipping without using next explicitly, but it’s nice to have on hand.

4.3.6 Repeat

For-loops are pretty wonderful, but they are best used in circumstances when you know how many times you will need to loop. When you need to repeat a block of code until a certain condition occurs, then the repeat reserved-word might be a good choice.

For example, suppose you want to play the number-guessing game with the user, but let her keep guessing until either she gives up or gets the correct answer. Here’s an implementation using repeat:

n <- 20
number <- sample(1:n, size = 1)
cat("I'm thinking of a whole number from 1 to ", n, ".\n", sep = "")
repeat {
  guess <- readline("What's your guess? (Enter q to quit.)  ")
  if ( guess == "q"  ) {
    cat("Bye!\n")
    break
  } else if ( as.numeric(guess) == number ) {
    cat("You are correct!  Thanks for playing!")
    break
  }
  # If we get here, the guess was not correct:
  # loop will repeat!
}

The game works well enough, but if you give it a try, you are sure to find it a bit fatiguing. It wold be nice to give the user a hint after an incorrect guess. Let’s revise the game to tell the reader whether her guess was high or low. While we are at it, let’s cast the game into the form of a function.

numberGuess <- function(n) {
  number <- sample(1:n, size = 1)
  cat("I'm thinking of a whole number from 1 to ", n, ".\n", sep = "")
  repeat {
    guess <- readline("What's your guess? (Enter q to quit.)  ")
    if (guess == "q") {
      cat("Bye!\n")
      break
    } else if (as.numeric(guess) == number) {
      cat("You are correct!  Thanks for playing!")
      break
    }
    # If we get to this point the guess was not correct.
    # Issue hint:
    hint <- ifelse(as.numeric(guess) > number, "high", "low")
    cat("Your guess was ", hint, ".  Keep at it!\n", sep = "")
    # Repeat loop
  }
}

A typical game:

> numberGuess(100)
I'm thinking of a whole number from 1 to 100.
What's your guess? (Enter q to quit.)  50
Your guess was high.  Keep at it!
What's your guess? (Enter q to quit.)  25
Your guess was high.  Keep at it!
What's your guess? (Enter q to quit.)  12
Your guess was low.  Keep at it!
What's your guess? (Enter q to quit.)  18
Your guess was low.  Keep at it!
What's your guess? (Enter q to quit.)  22
Your guess was high.  Keep at it!
What's your guess? (Enter q to quit.)  20
You are correct!  Thanks for playing!

4.3.7 While

The reserved word while constructs a loop that runs as long as a specified condition is true. Unlike repeat, which launches directly into the loop, the condition for while is evaluated prior to the body of the loop. If the condition is false at the beginning, the code in the body of the loop is never executed.

verboseSearch() could be re-written with while:

verboseSearch <- function(elem, vec) {
  found <- FALSE
  if ( length(vec) == 0 ) {
    cat("The vector is empty.  No way does it contain ", 
        elem, ".\n", sep = "")
    return(-1)
  }
  # index of vec (start looking at 1):
  i <- 1
  while ( !found & i <= length(vec) ) {
    if ( vec[i] == elem ) {
      found <- TRUE
      break
    }
    cat("No match at position ", i, " ...\n")
    i <- i + 1
  }

  if ( found ) {
    # report success:
    cat("Found ", elem, " at index ", i, ".\n", sep = "")
    return(i)
  } else {
    # report failure:
    cat(elem, " is not in the vector.\n", sep = "")
    return(-1)
  }
}

4.3.8 Practice Problems

  1. You want to compute the square roots of the whole numbers from 1 to 10. You could do this quite easily as follows:

    sqrt(1:10)
    ##  [1] 1.000000 1.414214 1.732051 2.000000 2.236068 2.449490 2.645751
    ##  [8] 2.828427 3.000000 3.162278

    However, you want to practice using for-loops. Write a small program that uses a loop to compute the square roots of the whole numbers from 1 to 10.

  2. Think about the practice problem findKarl() from the previous section. Write a new version that uses a loop.

  3. You want to append "the Dog" to the end of each element of a character vector of dog-names. Sadly, you have not yet realized that the paste() function is very much up to the task:

    doggies <- c("Bo", "Gracie", "Rover", "Anya")
    paste(doggies, "the Dog")
    ## [1] "Bo the Dog"     "Gracie the Dog" "Rover the Dog"  "Anya the Dog"

    You decide to use a for-loop. Write the code.

  4. Write a function called triangleOfStars() that produces patterns liks this:

    ****
    ***
    **
    *

    It should take a single parameter n, the number of asterisks in the first “line” of the triangle. Use a for-loop in the body of the function.

  5. Write a function called trianglePattern() that produces patterns like the ones from the previous problem, but using any character that the user specifies. The function should take two parameters:
    • char, a string giving the character to use. The default value should be "*".
    • n, the number of asterisks in the first “line” of the output.

    Typical examples of use should be as follows:

    trianglePattern(char = "x", 5)
    ## xxxxx
    ## xxxx
    ## xxx
    ## xx
    ## x
    trianglePattern(n = 6)
    ## ******
    ## *****
    ## ****
    ## ***
    ## **
    ## *
  6. Rewrite triangleOfStars() so that it uses a while-loop in its body.

  7. Rewrite triangleOfStars() so that it still uses a while-loop in its body, but produces output like this:

    *
    **
    ***
    ****
  8. Write a function called cheerleader() that badgers you for each of the letters in a word until you give it all of the letters, one by one. It should work like this:

    schoolMascot <- c("T", "I", "G", "E", "R", "S")
    cheerleader(cheer = schoolMascot)
    > cheerleader(cheer = schoolMascot)
    Gimme a T!  T
    Gimme a I!  i
    Gimme a I!  I
    Gimme a G!  gee
    Gimme a G!  I said gee!!
    Gimme a G!  G
    Gimme a E!  E
    Gimme a R!  Are you going to do this all day?
    Gimme a R!  R
    Gimme a S!  Stop it, please!
    Gimme a S!  S
    > 
  9. Write a function called ageReporter() that reports the ages of given individuals. It should take two parameters:

    • people: a character vector of names of the people of interest.
    • ages: a vector containing the ages of the people of interest.

    The function should perform some validation: it should stop the user with a helpful message if the number of people supplied and the number of ages supplied are not the same, or if one of the supplied vectors has length zero.

    Typical examples of use would be as follows:

    folks <- c("Gina", "Hector")
    theirAges <- c(25, 22)
    ageReporter(folks, theirAges)
    ## Gina is 25 year(s) old.
    ## Hector is 22 year(s) old.
    folks <- c("Gina", "Hector", "Tom")
    theirAges <- c(25, 22)
    ageReporter(folks, theirAges)
    ## Number of people must equal number of ages!
    folks <- c("Gina", "Hector")
    theirAges <- numeric(0)
    ageReporter(folks, theirAges)
    ## Provide at least one age!

4.3.9 Solutions to the Practice Exercises

  1. Here’s a suitable program:

    roots <- numeric(10)
    for ( i in 1:10 ) {
      roots[i] <- sqrt(i)
    }
    roots
    ##  [1] 1.000000 1.414214 1.732051 2.000000 2.236068 2.449490 2.645751
    ##  [8] 2.828427 3.000000 3.162278
  2. You could do this:

    findKarl <- function(x) {
      results <- character(length(x))
      for ( i in 1: length(x) ) {
        if ( x[i] == "Karl" ) {
          results[i] <- "Yep, that's Karl!"
        } else {
          results[i] <-  "Sorry, not our guy."
        }
      }
      results
    }
  3. Here you go:

    doggies <- c("Bo", "Gracie", "Rover", "Anya")
    titledDoggies <- character(length(doggies))
    for (i in 1:length(doggies) ) {
      titledDoggies[i] <- paste(doggies[i], "the Dog")
    }
    titledDoggies
    ## [1] "Bo the Dog"     "Gracie the Dog" "Rover the Dog"  "Anya the Dog"
  4. Here’s the code:

    triangleOfStars <- function(n) {
      for ( i in n:1 ) {
        line <- rep("*", times = i)
        cat(line, "\n", sep = "")
      }
    }

    Try it out:

    triangleOfStars(5)
    ## *****
    ## ****
    ## ***
    ## **
    ## *
  5. Here’s the code:

    trianglePattern <- function(char = "*", n) {
      for (i in n:1) {
        line <- rep(char, times = i)
        cat(line, "\n", sep = "")
      }
    }
  6. Here’s the code:

    triangleOfStars <- function(n) {
      while ( n >= 1 ) {
        line <- rep("*", times = n)
        cat(line, "\n", sep = "")
        n <- n - 1
      }
    }

    Try it out:

    triangleOfStars(5)
    ## *****
    ## ****
    ## ***
    ## **
    ## *
  7. Here’s the code:

    triangleOfStars <- function(n) {
      m <- 1
      while ( m <= n ) {
        line <- rep("*", times = m)
        cat(line, "\n", sep = "")
        m <- m + 1
      }
    }

    Try it out:

    triangleOfStars(5)
    ## *
    ## **
    ## ***
    ## ****
    ## *****
  8. Here is the requested annoying function, using a repeat-loop with break inside a for-loop:

    cheerleader <- function(cheer) {
      for ( letter in cheer ) {
        repeat {
          call <- paste("Gimme a ", letter, "!  ", sep = "")
          response <- readline(call)
          if ( response == letter ) break
       }
      }
    }
  9. Here is one possibility:

    ageReporter <- function(people, ages) {
      ## validation:
      p <- length(people)
      a <- length(ages)
      if (p == 0) {
        return(cat("Provide at least one person!\n"))
      }
      if (a == 0) {
        return(cat("Provide at least one age!\n"))
      }
      if (!(p == a & p > 0)) {
        return(cat("Number of people must equal number of ages!\n"))
      }
      ## process:
      for (i in 1:length(people)) {
        cat(people[i], " is ", ages[i], " year(s) old.\n", sep = "")
      }
    }