Recall- non-standard evaluation
If you’re masking specific tidyverse functions, you can get away with { } and passing arguments along with ....
But if we’re doing more general-purpose programming, we need to leverage non-standard evaluation in R.
| capture unevaluated expression |
quote() |
expr() |
quo() |
| substitute name for value |
substitute() |
enexpr() |
enquo() |
| evaluate a captured expression |
eval() |
eval_tidy() |
!! |
There are many more NSE functions, but I find these three concepts usually get me pretty far.
Motivation for tidy eval
Tidy eval (and non-standard evaluation generally in R) exists so that we can refer to data columns using bare names.
dplyr::filter(mtcars, cyl == 4)
# python, using pandas
mtcars.query('cyl == 4')
- It makes things easier if you are working interactively.
- It makes things more interesting if you are writing functions.
Welcome to more interesting.
Families of tidy-eval functions
There are two families of tidy-eval functions:
Ways to express tidy-eval
Goal
Write functions that:
- call tidyverse functions, e.g.
dplyr::filter()
- “look and feel” like tidyverse functions:
- your users can call using bare names
- use string-techniques when we know the column names
Out of scope:
- underlying theory of quasiquotation, i.e.
rlang::quo(), rlang::enquo(), rlang::eval_tidy()
Pass the dots
This is the simplest possible solution.
- If the tidyverse function you’re using takes
... as an argument,
- and that’s what you want to pass along,
then you can pass the dots.
my_select <- function(.data, ...) {
dplyr::select(.data, ...)
}
Pronouns: .data, .env
library("dplyr")
mtcars |> nrow()
[1] 32
mtcars |> filter(cyl == 4) |> nrow()
n_cyl <- 4
mtcars |> filter(cyl == n_cyl) |> nrow()
Pronouns: .data, .env (cont.)
n_cyl <- 4
mtcars |> filter(cyl == n_cyl) |> nrow()
[1] 11
cyl <- 4
filter(mtcars, cyl == cyl) |> nrow()
mtcars |> filter(.data$cyl == .env$cyl) |> nrow()
Pronouns
The .data and .env pronouns are provided by {rlang}.
For data-masking functions, make explicit how to evaluate variables.
To use them in your package:
usethis::use_import_from("rlang", c(".data", ".env"))
Splicing: !!!
Used to splice a list or vector into a ... argument.
It works only with dynamic dots (a tidyverse construct).
library("dplyr")
mtcars |> rename(new_mpg = "mpg", new_cyl = "cyl")
new_names <- c(new_mpg = "mpg", new_cyl = "cyl")
mtcars |> rename(!!!new_names)
!!! reflects ...
!!! evaluates the variable using the environment
New columns
With dynamic dots you can name new columns using variables.
prefix <- "new"
mtcars |>
dplyr::rename("{prefix}_mpg" := "mpg")
Two things to keep in mind:
- left side of expression is a {glue}-enabled string.
- operator is
:=, not =.
To use in your package:
usethis::use_import_from("rlang", ":=")
dplyr::across(): super useful
mtcars |>
dplyr::group_by(dplyr::across("cyl")) |>
dplyr::summarise(
dplyr::across(c("mpg", "disp"), mean)
)
Tidy-select and ambiguity
library("dplyr")
mtcars |> select(cyl) |> glimpse()
placeholder <- c("wt", "mpg")
mtcars |> select(placeholder) |> glimpse()
cyl <- c("wt", "mpg")
mtcars |> select(cyl) |> glimpse()
mtcars |> select(all_of(cyl)) |> glimpse()
all_of(), any_of()
Exported by {dplyr}; part of {tidyselect}
Used to disambiguate external- vector of column names:
placeholder <- c("wt", "mpg", "not_in_mtcars")
all_of() is strict:
mtcars |> select(all_of(placeholder)) |> glimpse()
any_of() is permissive:
mtcars |> select(any_of(placeholder)) |> glimpse()
Curly-curly: {{}}
glue lets you interpolate strings:
greet <- function(name) {
glue::glue("Hello {name}")
}
Tidy eval lets you interpolate arguments:
mutate_greet <- function(.data, greeting) {
.data |>
dplyr::mutate(greeting = {{ greeting }})
}
Try out greeting functions
library("palmerpenguins")
my_name <- "Amelia"
penguins |> mutate_greet(my_name) # works with vars in environment
penguins |> mutate_greet(species) # works with vars in data
# compute on data
penguins |> mutate_greet(paste(species, island))
# getting a little silly
penguins |>
mutate_greet(paste(species, island) |> greet())
Curly-curly, revisited
mutate_greet <- function(.data, greeting) {
.data |>
dplyr::mutate(greeting = {{ greeting }})
}
{{}}:
- freezes the expression, capturing context
- delays evaluation
- function you call must be capable
- tidyverse data-masking and tidy-select functions are capable
Summary
There are a lot of tidy-eval tools:
- know if you are using a data-masking or tidy-select function.
- data-masking function using tidy-select syntax:
across()
- to move a bunch of arguments, pass the dots,
...
- to splice a list (or vector) into dynamic-dots, use
!!!
- to interpolate a single argument, use
{{}}
Summary: disambiguation
For data-masking functions, use .data, .env pronouns:
library("dplyr")
cyl <- 4
mtcars |> filter(.data$cyl == .env$cyl)
For tidy-select functions, use all_of(), any_of():
cyl <- c("mpg", "wt")
mtcars |> select(all_of(cyl))