Previous chapter
Writing FunctionsControl Flow
Next chapter

if and else

Control Flow

Control flow refers to the order in which a function executes the statements in its body of code.

By default, R functions will execute each line of code in the body in order, and then return the result of the last line of code. But it doesn’t have to be this way. You can write functions that run some code in some situations and other code in other situations.

Let’s learn how!

if

Take a look at the code below. What is happening?

x <- 1
x == 1
## [1] TRUE
x > 1
## [1] FALSE
log(x)
## [1] 0
if (x == 1) log(x)
## [1] 0
if (x > 1) log(x)

Try to describe what if is doing in your own words. Your best guess is okay!

Data cleaner

Think you have it? Let’s check. We’ll use if to write a useful function.

Many data sets use their own symbols to represent missing values. For example, NOAA will often use -99 to represent missing values in weather data sets. Let’s write a function that checks whether a value is -99, and if so replaces the value with NA, like this:

clean(1)
## [1] 1
clean(-99)
## [1] NA

Here is a start. clean() takes an object and returns the object, but clean() is missing an important piece of code.

  • Add an if statement to the beginning of clean(). Your statement should assign NA to x if x equals -99. Then click Submit Answer.
x <- -99
clean <- function(x) {
  # add if statement here
  x
}
"Don't forget to use == to check for equality."
clean <- function(x) {
  if (x == -99) x <- NA
  x
}
strict_check("clean() replaces x with NA if x is -99, otherwise clean() returns x as is.")

else

Here is a second version of clean() that uses on a new command. Can you tell what else does?

clean <- function(x) {
  if (x == -99) NA else x
}
  • Run clean() with several different values, 22, -99, 3. What does clean() return in each case? Why?
clean <- function(x) {
  if (x == -99) NA else x
}

A word about syntax

Although you can put if and else on the same line, you shouldn’t because it is easy for readers to miss the trailing else when they scan the code. Also, placing if and else on the same line can make very long lines.

It would be more common to write our function like this:

clean <- function(x) {
  if (x == -99) NA 
  else x
}

R parses the if and else lines as a single statement as long as else is the first thing that follows the if statement. As a result, R will return the result of the combined if else statement if it appears at the end of a function.

{}

You can also pass if and else chunks of code surrounded by braces, {}. Braces group multiple lines of code into a single “piece.” When you use braces in an if else statement, R will run (or not run) everything between the braces.

In this example, R will run all three lines that follow else whenever x does not equal -99.

clean <- function(x) {
  if (x == -99) {
    NA
  } else {
    x <- x^2
    x <- sqrt(x)
    x
  }
}

When you use braces, indent everything between the braces by two spaces to make your code more readable. And of course, you can use braces to organize your code even if you have a single line of code between the braces.

If else quiz

SAS often saves missing values as ".".

  • Write a function named clean2 that takes a value named x and returns an NA if the value is "." (and returns the value of x otherwise).
x <- "."
"clean2() should closely resemble clean()."
clean <- function(x) {
  if (x == ".") NA 
  else x
}
strict_check("What if you'd like to check for both -99 and . in the same function?")

else if

In that case, you can use else to chain together multiple if statements.

clean <- function(x) {
  if (x == -99) NA 
  else if (x == ".") NA
  else x
}

This does the same thing as

clean <- function(x) {
  if (x == -99) NA 
  else {
      if (x == ".") NA
      else x
  }
}

clean() will:

  1. Check whether x == -99. If so clean() will return NA and skip the rest of the code. If not, clean() will…
  2. Check whether x == ".". If so, clean() will return NA and skip the rest of the code. If not, clean() will…
  3. Evaluate x and return its value.

else if is more readable than nested if else statements, especially if you use many else ifs.

You can use else to string together as many if statements as you like. R will treat the result as a single multi-part if else statement. Be thoughtful with the order. R will always evaluate the clauses in order, executing the code in the first clause whose condition is true and ignoring every clause after that.

Your turn

  • Write a function named clean() that uses if, else, and else if statements to replace the following four values with NA before returning x, -99, ".", "", "NaN". Then click Submit Answer.
x <- ""
clean <- function(x) {
  if (x == -99) NA 
  else if (x == ".") NA
  else if (x == "") NA
  else if (x == "NaN") NA
  else x
}
strict_check("You can add as many else if's after if as you like. However, you cannot add an else if clause after an else because R will interpret the else as the end of a complete if else statement.")

Quiz

foo <- function(x) {
  if (x > 2) "a"
  else if (x < 2) "b"
  else if (x == 1) "c"
  else "d"
}
foo(1)

Quiz

clean <- function(x) {
  if (x == -99) NA 
  if (x == ".") NA
  if (x == "NaN") NA
  x
}
clean(-99)

Congratulations!

You now know how to use if and else in your code. Let’s look at another way to control flow in R.

return() and stop()

You can tell R to stop executing a function early with

  • return()
  • stop(), and
  • stopifnot()

Each will work only in the context of a function (because they stop the function). You wouldn’t run these directly at the command line, but they provide a powerful way to control the flow of your functions: They can make if else statements unnecessary and they can even make your code less buggy.

return()

When R encounters return() it will stop executing the function that called return(). If you pass a value to return(), R will return that value when it stops executing the function. Let’s see how it works.

impatient_square <- function(x) {
  return(x)
  x^2
}

R with a python accent

If you are a python user, you might already use return() …unnecessarily. In python, you explicitly tell each function what to return, e.g.

def mysquare(x):
  y = x * x
  return y

Translated to R this becomes:

my_square <- function(x) {
  y <- sum(x) / length(x)
  return(y)
}

But in R, this return() is not needed. R functions automatically return the result of their last line of code. In R, you can save return() for unusual control flow.

Using return()

Remember this function? It didn’t work as expected because we forgot to link our if statements with else.

  • Fix the function not by adding else, but by adding return() in the right places. Then Click Submit Answer.
clean <- function(x) {
  if (x == -99) NA 
  if (x == ".") NA
  if (x == "NaN") NA
  x
}
clean <- function(x) {
  if (x == -99) return(NA)
  if (x == ".") return(NA)
  if (x == "NaN") return(NA)
  x
}
strict_check("This version is slightly easier to read than a linked if else tree because you can think about each if clause separately. You can often avoid long, nested if else trees by using return() thoughtfully.")

NULL

clean() is a fairly useful function, but it does have one flaw.

  • What happens when x = NULL? Run the code and find out.
clean <- function(x) {
  if (x == -99) return(NA)
  if (x == ".") return(NA)
  if (x == "NaN") return(NA)
  x
}
clean(NULL)
clean <- function(x) {
  if (x == -99) return(NA)
  if (x == ".") return(NA)
  if (x == "NaN") return(NA)
  x
}
clean(NULL)

clean() cannot handle NULL because if returns an error when it evaluates NULL == -99. And, unfortunately, the error message isn’t very clear. This is the perfect case for stop().

clean(NULL)
## Error in if (x == -99) return(NA): argument is of length zero

stop()

stop() behaves like return(), but instead of returning a value, stop() returns an error, complete with a custom error message. Can you tell how it works?

immovable_square <- function(x) {
  stop("I refuse to proceed.")
  x^2
}

Use stop()

  • Use if and is.null() to add a stop() call at the beginning of clean(). The command should return the error message "x is NULL" whenever x is NULL.
  • Then click Submit Answer.
clean <- function(x) {
  if (x == -99) return(NA)
  if (x == ".") return(NA)
  if (x == "NaN") return(NA)
  x
}
clean <- function(x) {
  if (is.null(x)) stop("x is NULL")
  if (x == -99) return(NA)
  if (x == ".") return(NA)
  if (x == "NaN") return(NA)
  x
}
strict_check("Now `clean()` can handle NULL values in an intelligent way. This pattern (if + stop) is so common that R provides a shortcut for it, `stopifnot()`.")

stopifnot()

stopifnot() is a more readable substitute for statements that combine if and stop(). Can you guess how it works?

differences

stopifnot() is different from if + stop() in a few important ways:

  1. Instead of checking whether a condition is met, stopifnot() checks whether a condition is not met.
  2. stopifnot() does not pass along a custom error message. Instead, stopifnot() always explains that the condition was not true:

    x <- -1
    stopifnot(x >= 0)
    ## Error: x >= 0 is not TRUE

Notice that the first argument of stopifnot() should always be a logical condition, the inverse of the condition it replaces in an if + stop() statement.

You can include additional logical conditions for stopifnot() to check after the first. Separate each with a comma.

Think you have it?

  • Try replacing the if + stop() statement in clean() with stopifnot(). Then click Submit Answer.
clean <- function(x) {
  if (is.null(x)) stop("x is NULL")
  if (x == -99) return(NA)
  if (x == ".") return(NA)
  if (x == "NaN") return(NA)
  x
}
"You can reverse the result of `is.null()` by placing an `!` in front of it: `!is.null()`."
clean <- function(x) {
  stopifnot(!is.null(x))
  if (x == -99) return(NA)
  if (x == ".") return(NA)
  if (x == "NaN") return(NA)
  x
}
strict_check("stopifnot() is both very readable and very useful. Click Continue to see why.")

Defensive programming with stopifnot()

You can save yourself debugging time by writing your functions to fail fast with clear error messages. To do this, think about situations that will lead to errors and then check for them with stopifnot() at the beginning of your code.

clean <- function(x) {
  stopifnot(!is.null(x), is.numeric(x), length(x) == 1)
  
  if (x == -99) return(NA)
  x
}

If things go wrong, stopifnot() will help you see what you need to fix as soon as you run your function. Compare this to what will happen if you do not use stopifnot():

  1. Your code will run until it triggers a (perhaps unhelpful) error message
  2. Your code may not trigger an error message, but return an incorrect result that you will think is true. This would very bad.

Congratulations! You know two techniques of control flow, how to:

  1. Run specific code in specific cases
  2. Stop execution early

In the Advanced Control Flow tutorial, you’ll learn how to combine logical tests in an if statement as well as how to write if statements that work with vectors, which is a prerequisite if you want to write vectorized functions.