4

I'm trying to wrap my head around non-standard evaluation as it's interpreted in the rlang package. With that goal in mind, my question is:

How do I write a dplyr::select.list() function that is consistent with tidy evaluation principles?

Here's an example of how I would currently write a wrapper around dplyr::select():

select_wrapper <- function(x, ...) {
  vars <- rlang::quos(...)
  dplyr::select(x, !!!vars)
}

That works on data frames, e.g.,

> select_wrapper(mtcars, cyl, mpg)
> ##                     cyl  mpg
> ## Mazda RX4             6 21.0
> ## Mazda RX4 Wag         6 21.0
> ## Datsun 710            4 22.8
> ## Hornet 4 Drive        6 21.4
> ## Hornet Sportabout     8 18.7
> ## Valiant               6 18.1

But not on lists:

attr(mtcars, "test") <- "asdf"
mtcars_list <- attributes(mtcars)
select_wrapper(mtcars_list, row.names, test)
> ## 1: c("mpg", "cyl", "disp", "hp", "drat", "wt", "qsec", "vs", "am", "gear", "carb")
> ## 2: c("Mazda RX4", "Mazda RX4 Wag", "Datsun 710", "Hornet 4 Drive", "Hornet Sportabout", "Valiant", "Duster 360", "Merc 240D", "Merc 230", "Merc 280", "Merc 280C", "Merc 450SE", "Merc 450SL", "Merc 450SLC", "Cadillac Fleetwood", "Lincoln Continental", "Chrysler Imperial", "Fiat 128", "Honda Civic", "Toyota Corolla", "Toyota Corona", "Dodge Challenger", "AMC Javelin", "Camaro Z28", "Pontiac Firebird", "Fiat X1-9", "Porsche 914-2", "Lotus Europa", "Ford Pantera L", "Ferrari Dino", "Maserati Bora", "Volvo 142E")
> ## 3: data.frame
> ## 4: asdf
> ## Selection: 

To be honest, I'm not sure what's going on in the output above...it returns an interactive prompt asking me to select which element I want. That's not really ideal, imo.

Anyway, what I'd like to accomplish, is a select.list() function that returns a list of the named elements I select via non-standard evaluation. This is my solution, but it feels too hacky:

listdf <- function(x) {
  as.data.frame(lapply(x, function(x) I(list(x))))
}
dflist <- function(x) {
  x <- lapply(x, unlist, recursive = FALSE)
  lapply(x, unclass)
}
select.list <- function(x, ...) {
  dots <- rlang::quos(...)
  if (length(dots) == 0L) return(list())
  x <- listdf(x)
  dflist(dplyr::select(x, !!!dots))
}

library(dplyr)
attr(mtcars, "test") <- "asdf"

select(attributes(mtcars), test, row.names)

Is there a cleaner and more tidy-eval consistent way to do this?

mkearney
  • 1,266
  • 10
  • 18

2 Answers2

6

You can use tidyselect which implements the backend for select():

select2 <- function(.x, ...) {
  vars <- rlang::names2(.x)
  vars <- tidyselect::vars_select(vars, ...)
  .x[vars]
}

x <- list(a = 1, b = 2)
select2(x, dplyr::starts_with("a"))

Note that it's bad practice to implement an S3 method when you don't own either the generic (e.g. select() owned by dplyr) or the class (e.g. list from R core).

Lionel Henry
  • 6,652
  • 27
  • 33
  • 1
    thank you for this! it'll be a wrapper for select for a new [list-like] class. i just didn't want to have to explain all of that for the question :). – mkearney Feb 08 '18 at 18:33
  • Working a lot with lists, I've been looking for this for a very long time! Any idea why has the `purrr` package, which represents the list side of the `tidyverse`, never implemented such a convenient function? – Dan Chaltiel Oct 20 '19 at 09:03
  • 2
    Good timing with your question! I'm thinking about implementing a generic `tidyselect::vec_select()` which will work with lists and any vector. – Lionel Henry Oct 21 '19 at 07:57
0

I think dplyr is for dfs

The reason select does super weird stuff with lists is because it wasn't really made for that. I'm not even sure why that happens (I ran into that interactive list thing once and was soooo confused).

[ is select for lists

But [ takes strings. So your problem is really to convert bare arguments to strings for use in [:

library(tidyverse)
l <- letters
names(l) <- letters
l
select.list <- function(x, ...) {
  vars <- rlang::quos(...) %>% map(quo_text) %>% unlist()
  x[vars]
}

select.list(l, a, b)
  a   b 
"a" "b" 

Here, quos returns a list of quosores; to that, I mapped quo_text to convert each element from a quosure to a string, and then unlisted the elements. Then you can call [ on the list directly, x[vars] to return a list of the named elements.

twedl
  • 1,588
  • 1
  • 17
  • 28
  • I've come up with similar solutions (converting to character strings via `deparse` or `exprs`), but I didn't know about `quo_text`! I wonder, though, if these solutions are [hygienic](https://www.r-project.org/dsc/2017/slides/tidyeval-hygienic-fexprs.pdf) (or [chap 5 here](https://web.wpi.edu/Pubs/ETD/Available/etd-090110-124904/unrestricted/jshutt.pdf))? – mkearney Feb 08 '18 at 17:55
  • Also, I agree dplyr wasn't made for lists. I'm putting together functions that preserve attributes, so translating some dplyr methods to lists would be helpful. – mkearney Feb 08 '18 at 18:01
  • Hmm. It sounds like you need to write unit tests for the things you're looking for and test various solutions against them. Also by "translate dplyr methods", do you want to just wrap a dplyr method and call it on a list, or do you want to look at the dplyr source code and translate that directly? Either one might be super tough, so good luck – twedl Feb 08 '18 at 18:08
  • Yeah I'm not trying to recreate dplyr :). But I'm trying to use the task (functions for preserving data frame attributes) to learn rlang/tidy eval. So my tidy eval-question is more theoretical–whether converting names to character strings is considered hygeinic–and less about unit testing (which I agree would still be important). – mkearney Feb 08 '18 at 18:12