💻 🏦
Code You Can Bank On

Tidy eval

Amelia McNamara

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.

purpose base rlang rlang (quosure)
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.

Tidy eval

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:

  • data-masking: evaluate variables in context of data frame

    mtcars |>
      dplyr::mutate(wt_kg = wt * 1000 / 2.2)
  • tidy-select: specify columns within a data frame

    mtcars |>
      dplyr::select(starts_with("w"))

Ways to express tidy-eval

  • using bare names, what we do in interactive programming:

    mtcars |>
      dplyr::mutate(wt_kg = wt * 1000 / 2.2)
  • using strings, useful for Shiny :

    mtcars |>
      dplyr::mutate("wt_kg" := .data[["wt"]] * 1000 / 2.2)

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()
[1] 11

n_cyl <- 4
mtcars |> filter(cyl == n_cyl) |> nrow()
[1] 11

Pronouns: .data, .env (cont.)

n_cyl <- 4
mtcars |> filter(cyl == n_cyl) |> nrow()
[1] 11

cyl <- 4
filter(mtcars, cyl == cyl) |> nrow()
[1] 32

mtcars |> filter(.data$cyl == .env$cyl) |> nrow()
[1] 11

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

  • used inside data-masking verbs, e.g. summarise()

  • specify:

    • which columns to consider, using tidy-select

    • what function(s), if any, to apply

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}")
}
greet("there")

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))

References