Motivation
Provide some examples of using Tidy evaluation (also called non-standard evaluation (NSE) or delayed evaluation).
ℹ️ This post may be useful for your if you have read Chapters 17-20 of Advanced R book and you are looking to find more additional examples of Tidyeval. You may also want to have a look at the great Tidyeval resource put together by Mara.
Libraries needed for this post
library(rlang)
library(tidyverse)
library(testthat)
Standard function to coefficient of variation
Let’s write a function that calculates coefficient of variation:
cv <- function(var) {
sd(var) / mean(var)
}
We can test that the function is behaving correctly
testthat::expect_equal( cv(c(3,3)), 0)
testthat::expect_equal( round(cv(c(3,6)),2), 0.47)
Great! The function seems to be doing what we want!
Capture and uncapture expression
We can capture and uncapture expressions with enexpr
and bang-bang !!
cv <- function(var) {
var <- enexpr(var)
expr(sd(!!var) / mean(!!var))
}
There is a lot is going on here:
- enexpr: it captures what the caller supplied to the function and allows delayed evaluation.
- !!: it unquotes. Sort of like make available what it was captured by enexpr. No evaluation has happened yet.
- expr: it captures what it was unquoted by !!. No evaluation has happened yet.
Let’s see what happens:
testthat::expect_equal(eval(cv(c(3,3))), 0)
testthat::expect_type((cv(c(3,3))), "language")
If we evaluate the function, the cv is equal to 0. Note that we have delayed the evaluation up the point when used eval.
If we don’t evaluate the function it remains as a call object.
Modiying the enviroment - no issues
Let’s modify the function’s environment a little:
cv <- function(var) {
x=6
var <- enexpr(var)
expr(sd(!!var) / mean(!!var))
}
Let’s see what happens:
x=3
testthat::expect_equal( eval(cv(c(3,x))), 0)
x=6
testthat::expect_equal(round(eval(cv(c(3,x))),2), 0.47)
This works even if we put x in the environment where the function is written because x is not an argument of cv function.
Modiying the enviroment - ISSUES!
adding_cv <- function(df,var) {
x=c(3,6)
var <- enexpr(var)
mutate(df, sd(!!var) / mean(!!var))
}
Let’s see what happens:
df<- tibble(n=3)
x=c(3,3)
adding_cv(df,x)
## # A tibble: 1 x 2
## n `sd(x)/mean(x)`
## <dbl> <dbl>
## 1 3 0.471
Wait, what?! The new column contains coefficient of variation. If x=c(3,3) the value in the new column should have been 0. However adding_cv is using x=c(3,6) included in the function environment and not x=c(3,3).
Capturing with enquo
To capture the function and the environment we need enquo
adding_cv <- function(df,var) {
x=c(3,6)
var <- enquo(var)
mutate(df, sd(!!var) / mean(!!var))
}
Let’s see what happens now:
df<- tibble(n=3)
x=c(3,3)
adding_cv(df,x)
## # A tibble: 1 x 2
## n `sd(x)/mean(x)`
## <dbl> <dbl>
## 1 3 0
Changing name with :=
We can make the name of the new column prettier with :=
adding_cv <- function(df,var,nm) {
x=c(3,3)
nm_name <- quo_name(nm)
var <- enquo(var)
mutate(df, !!nm_name:= sd(!!var) / mean(!!var))
}
Let’s see if that allows changing the name:
x=c(3,6)
df<- tibble(n=3)
adding_cv(df,x,"pretty_name")
## # A tibble: 1 x 2
## n pretty_name
## <dbl> <dbl>
## 1 3 0.471
Insert a list of expressions into a call
What if we want to do delayed filtering? We need to unquote multiple arguments. For this we can use !!!
adding_cv <- function(df,var,nm, ...) {
x=c(3,6)
nm_name <- quo_name(nm)
var <- enquo(var)
filtering <- enquos(...)
df %>% filter(!!!filtering) %>%
mutate( !!nm_name:= sd(!!var) / mean(!!var))}
Let’s see if we can filter rows 3 and 6 from column n of our df:
df<- tibble(n=c(3,6,9),m=c(3,6,9))
x=c(3,3)
adding_cv(df,x,"pretty_name", n %in% c(3,6))
## # A tibble: 2 x 3
## n m pretty_name
## <dbl> <dbl> <dbl>
## 1 3 3 0
## 2 6 6 0
Conclusions
These were just few examples to illustrate why tidyeval can be useful and when it might be needed. I hope it helped you!