Tidyeval - passing multiple arguments without using ...

I am building a function that requires the user to occasionally pass multiple variables into an argument. The function already uses ... for something else.

I was trying to do this using {{ }} but passing multiple variables to {{ }} only works sometimes.

Here are two functions that both use {{ }}. Both functions work when the user passes a single variable to {{ }}, but the second function fails when passed multiple variables.

Can you help me:

  1. Understand why the first function succeeds, and the second fails when given multiple variables?
  2. fix function 2 so I can pass multiple arguments without using ...
library(tidyverse)

# Function 1
select_and_summarise <- function(.data, selected, ...){
  .data %>% 
    select({{selected}}) %>% 
    summarise_all(list(...))
}

# Function 2
group_and_summarise <- function(.data, group, ...){
  .data %>% 
    group_by({{group}}) %>% 
    summarise_all(list(...))
}


# Passing single variables work
mtcars %>% 
  select_and_summarise(selected = cyl, mean = mean)

mtcars %>% 
  group_and_summarise(group = cyl, mean = mean)


# Passing more than one variable only works for select_and_summarise
mtcars %>% 
  select_and_summarise(selected = c(cyl, mpg, hp), mean = mean)

mtcars %>% 
  group_and_summarise(group = c(cyl, mpg, hp), mean = mean)

EDIT
Following Hadleys advice I made this function that works for both the examples I've given:

group_and_summarise <- function(.data, group, ...){
  .data %>% 
    group_by_at(vars({{group}})) %>% 
    summarise_all(list(...))
}
1 Like

This is a tough one, hopefully @hadley can help. I can't answer your first question, but I made function 2 work with multiple arguments:

# Function 2
group_and_summarise <- function(.data, group, ...){
	
	group_expr <- enexpr(group)
	
	if(length(group_expr) == 1) { # one-variable case 
		group_vars <- as.list(group_expr)
	} else { # many-variable case
		group_vars <- as.list(group_expr)[-1]
	}
	
	.data %>%
		group_by(!!!group_vars) %>%
		summarise_all(list(...))
}

The select_and_summarise also then works like this:

# Function 1
select_and_summarise <- function(.data, selected, ...){
	
	selected_expr <- enexpr(selected)

	if(length(selected_expr) == 1) {
		selected_vars <- as.list(selected_expr)
	} else {
		selected_vars <- as.list(selected_expr)[-1]
	}
	
	.data %>% 
		select(!!!selected_vars) %>% 
		summarise_all(list(...))
}
1 Like

The first function succeeds because select() is designed this way — select(df, c(x, y, z)) is a valid select call. The second function fails because group_by(df, c(x, y, z)) is not a valid call to group_by(). One way to resolve your problem would be to use group_by_at() which uses selection semantics (like select()), rather than action semantics (like group_by()/mutate()). This would mean that the user can't create new grouping variables, but I think that's a sacrifice you have to make.

@valeri unfortunately your approach will fail if the user groups by a function of existing variables (as a call object has length equal to the number of arguments plus one).

4 Likes

Thank you. @hadley
That makes sense.

Following your advice I made this function:

group_and_summarise <- function(.data, group, ...){
  .data %>% 
    group_by_at(vars({{group}})) %>% 
    summarise_all(list(...))
}

It works for the given examples:

mtcars %>% 
  group_and_summarise(group = cyl, mean)

mtcars %>%
  group_and_summarise(group = c(cyl, hp), mean)
2 Likes

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.