Previous chapter
Next chapter

&& and ||

This tutorial extends the Control Flow tutorial, where you learned how to use `if`, `else`, `return()`, and `stop()`.

Here you will learn how to

1. combine logical tests in an if statement
2. write if statements that work with vectors, which is a prerequisite if you want to write vectorized functions.

Here’s what `clean()` looked like at the end of the Control Flow tutorial. Do you notice that all of the if statements have the same outcome?

``````clean <- function(x) {
stopifnot(!is.null(x))
if (x == -99) return(NA)
if (x == ".") return(NA)
if (x == "NaN") return(NA)
x
}``````

Let’s use your knowledge of logical tests to trim them down to a single if statement.

• Write a logical test that returns TRUE when x is -99 OR x is “.” (Let’s ignore the “NaN” case to keep things simple). Then click Submit Answer.
``"You can combine two logical tests in R with `&` (and) and `|` (or), e.g. x < 0 & x > 1."``
``x == -99 | x == "."``
``"This is the correct way to combine logical tests in R, but it has some downsides when you use it in an if statement."``

& and |

`&` and `|` are R’s boolean operators for combining logical tests.

• `&` stands for “and” will return `TRUE` if both tests return `TRUE` and will return `FALSE` otherwise.
• `|` stands for “or” will return `TRUE` if one or both tests returns `TRUE` and will return `FALSE` otherwise.

So,

``````x <- -99
x == -99 | x == "."``````
``## [1] TRUE``

However, it is bad practice to use `&` and `|` to combine logical tests within an `if` condition. Why? Because:

1. there is something better (as you’ll see in a minute)
2. `&` and `|` tend to generate warning messages when used with `if`

As R operators, both `&` and `|` are vectorized which means that you can use them with vectors. This is very useful.

``````x <- c(-99, 0 , 1)
x == -99``````
``## [1]  TRUE FALSE FALSE``
``x == "."``
``## [1] FALSE FALSE FALSE``
``x == -99 | x == "."``
``## [1]  TRUE FALSE FALSE``

However, `if` conditions are not vectorized. `if` expects the logical test contained within its parentheses to return a single `TRUE` or `FALSE`. If the condition returns a vector of `TRUE` or `FALSE`s, `if` will use the first value and show a warning message.

``````x <- c(-99, 0 , 1)
if (x == -99 | x == ".") NA``````
``````## Warning in if (x == -99 | x == ".") NA: the condition has length > 1 and
## only the first element will be used``````
``## [1] NA``

&& and ||

You can avoid this by always using `&&` and `||` within your `if` conditions. `&&` and `||` are lazy substitutes for `&` and `|`. They are lazy in two ways.

First, `&&` and `||` always return a single `TRUE` or `FALSE`. If you give `&&` or `||` vectors, they will compare only the first elements of the vectors—and they will not return a warning message.

``````x <- c(-99, 0 , 1)
x == -99 || x == "."``````
``## [1] TRUE``

Use ||

Let’s use this to our immediate advantage.

• Replace the two `if` statements below with a single statement that tests whether x is `-99` or `"."` without throwing error messages.
``````clean <- function(x) {
stopifnot(!is.null(x))
if (x == -99) return(NA)
if (x == ".") return(NA)
x
}``````
``"Like |, || expects a _complete_ logical test on each side of ||."``
``````clean <- function(x) {
stopifnot(!is.null(x))
if (x == -99 || x == ".") return(NA)
x
}``````
``strict_check("Now lets see what happens if you use clean() with a vector of values.")``
``````clean <- function(x) {
stopifnot(!is.null(x))
if (x == -99 || x == ".") return(NA)
x
}``````

Computation

The most important reason to use `||` instead of `|` is that `||` saves unnecessary computation when possible. This is the second way that `&&` and `||` are lazy.

When possible, `&&` and `||` jump to the correct conclusion after evaluating the first of the two logical tests (not so with `&` and `|`).

• `&&` will return `FALSE` if the test on the left returns `FALSE` (because the combined test would return `FALSE`).
• `||` will return `TRUE` if the test on the left returns `TRUE` (because the combined test would return `TRUE`)

In either case, `&&` and `||` will not evaluate the test on the right.

``````x <- -99
if (x == -99 || stop("if you evaluate this.")) "I didn't evaluate stop()."``````
``## [1] "I didn't evaluate stop()."``

How could you use this?

Remember how this code returns an error because `if` cannot handle the result of `NULL == -99`?

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

Quiz

Suppose we redefine `clean()` like this:

``````clean <- function(x) {
if (is.null(x) || x == -99) return(NA)
x
}``````

Vectorized if

Buried in the last section is an interesting question: what if you do want `clean()` to work with vectors? i.e.

``clean(c(-99, 0, 1))``
``## [1] NA  0  1``

That would be a handy way to clean whole columns of data. How could you do it?

Compare these two functions (one should seem familiar). What is different?

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

clean2 <- function(x) {
ifelse(x == -99, NA, x)
}``````

ifelse()

`ifelse()` is a function that replicates an if else statement. It takes three arguments: a logical test followed by two pieces of code. If the test returns `TRUE`, `ifelse()` will return the results of the first piece of code. If the test returns `FALSE`, `ifelse()` will return the results of the second piece of code.

So `clean(-99)` and `clean2(-99)` both return `NA`.

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

However, unlike `if` and `else`, `ifelse` is vectorized. As a result, you can pass `ifelse()` a vector of values and it will apply the implied if else statement separately to each element of the vector.

``````x <- c(-99, 0, 1)
ifelse(x == -99, NA, x)``````
``## [1] NA  0  1``

`clean2()` inherits this vectorized property from `ifelse()`.

``clean2(c(-99, 0, 1))``
``## [1] NA  0  1``

Compare that to `clean()` (which is non-vectorized because it relies on `if` and `else`, which are non-vectorized).

``clean(c(-99, 0, 1))``
``````## Warning in if (x == -99) NA else x: the condition has length > 1 and only
## the first element will be used``````
``## [1] NA``

if_else

The dplyr package offers a slight improvement on `ifelse()` named `if_else()`. `if_else()` is faster than `ifelse()`, but it requires you to make sure that each case in the if else statement returns the same type of object. For example, the statement needs to return a real number (or a string, or a logical, etc.) whether or not the condition is `TRUE`.

No big deal, right? Well kind of.

``````x <- c(-99, 0, 1)
if_else(x == -99, NA, x)``````
``## Error: `false` must be a logical vector, not a double vector``

NA

What happened? Recall that data in R comes in six atomic types.

It is true:

``typeof(NA)``
``## [1] "logical"``

So when you write `if_else(x == -99, NA, x)`, `if_else()` returns a logical in the first case and a double (real number) in the second (assuming `x` is a real number).

You can get around this mishap in two ways:

1. Stick to `ifelse()`
2. Use a NA that comes with a type

Types of NA

You may not realize it, but R comes with five types of NA. They all appear as `NA` when printed, but they are each saved with a separate data type. These are:

``NA # logical``
``## [1] NA``
``NA_integer_ # integer``
``## [1] NA``
``NA_real_ # double``
``## [1] NA``
``NA_complex_ # complex``
``## [1] NA``
``NA_character_ # character``
``## [1] NA``

You can fix `if_else()` by being precise about which NA to use (most other R functions will convert the type of NA without bothering you).

``````x <- c(-99, 0, 1)
if_else(x == -99, NA_real_, x)``````
``## [1] NA  0  1``

Use if_else

• Fix the `if_else()` statement of `clean2()` to work with real numbers. Then click Submit Answer.
``````clean2 <- function(x) {
ifelse(x == -99, NA, x)
}``````
``````clean2 <- function(x) {
ifelse(x == -99, NA_real_, x)
}``````
``strict_check("Notice that this version of `clean2()` will work with real numbers, but not other types of data like characters. What if you want `clean2()` to work with all types of data? That's simple: stick with `ifelse()`.")``

Vectorized else if

What if you want to write a vectorized version of a multi-part if else tree? Like the tree in this function:

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

In this case, neither `ifelse()` or `if_else()` will do. Why? Because each can only handle a single if condition, but our tree has four.

case_when()

You can vectorize multi-part if else statements with dplyr’s `case_when()` function. Here is how you would use `case_when()` to rewrite our `foo()` function from the Control Flow tutorial.

Here is the masterpiece in its original form

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

And here it is with `case_when()`.

``````foo2 <- function(x) {
case_when(
x > 2  ~ "a",
x < 2  ~ "b",
x == 1 ~ "c",
TRUE   ~ "d"
)
}``````

And here are our foos in action to prove that `foo2()` is vectorized.

``````x <- c(3, 2, 1)
foo(x)``````
``````## Warning in if (x > 2) "a" else if (x < 2) "b" else if (x == 1) "c" else
## "d": the condition has length > 1 and only the first element will be used``````
``## [1] "a"``
``foo2(x)``
``## [1] "a" "d" "b"``

Notice that

1. `case_when()` returns a single case for each element, the first case whose left hand side evaluates to `TRUE`
2. The left hand side of the last case evaluates to `TRUE` no matter what the value of `x` is (In fact, the left hand side is `TRUE`). This is an easy way to add an `else` clause to the end of `case_when()`.

Now let’s look at the unusual syntax of `case_when()`.

case_when() syntax

``````foo2 <- function(x) {
case_when(
x > 2  ~ "a",
x < 2  ~ "b",
x == 1 ~ "c",
TRUE   ~ "d"
)
}``````

Each argument of `case_when()` is a pair that consists of a logical test on the left hand side and a piece of code on the right hand side. The two are always separated by a `~`.

Like `if_else()`, `case_when()` expects each case to return the same type of output. So keep those NA types handy: `NA`, `NA_integer_`, `NA_real_`, `NA_complex_`, `NA_character_`.

Final Challenge

• Rewrite the multi-part version of `clean()` to use `case_when()`, which will allow `clean()` to handle vectors. Retain each case. Assume where necessary that `clean()` will only work with real numbers. Then click Submit Answer.
``````clean <- function(x) {
if (x == -99) NA
else if (x == ".") NA
else if (x == "") NA
else if (x == "NaN") NA
else x
}``````
``"Use NA's that have the right type."``
``````clean <- function(x) {
case_when(
x == -99 ~ NA_real_,
x == "." ~ NA_real_,
x == "" ~ NA_real_,
x == "NaN" ~ NA_real_,
TRUE ~ x
)
}``````
``strict_check('And if you noticed that a vector of real numbers would never contain ".", "", and "Nan" because they are strings, you are of course right. Thanks for playing along with the charade.')``

Congratulations!

You’ve learned how to alter the control flow of your functions with:

• `if`
• `else`
• `return()`
• `stop()`
• `stopifnot()`
• `ifelse()`

Not only that, you tackled two advanced methods: dplyr’s `if_else()` and dplyr’s `case_when()`.