2

I am trying to do something very similar to here.

Essentially, I need to pass a named list to a function, and give that named list to another function as its parameters.

If we follow that link, they are able to do it with mutate, and I can replicate that:

df <- tribble(
    ~a,
    1
)

foo <- function(x, args) {
    mutate(x, !!! args)
}

foo(df, quos(b = 2, c = 3)

# A tibble: 1 x 3
      a     b     c
  <dbl> <dbl> <dbl>
1     1     2     3

But if I try to do it with any other function, it fails. Say, if I try to use print, (which the first parameter is x, so I pass a named list with x in it):

print(x= "hello")
[1] "hello"

foo <- function(x, args) {
    print(!!! args)
}

foo(df, quos(x = "hello"))

Error in !args : invalid argument type

I'm not sure why this won't work outside of the "tidyverse" functions. I've tried different combinations of sym, enquo, bang bang, curly curly, etc., but to no avail.

Of course my final goal is not to use print but to use another user-defined-function in its place, so if you have any advice on how to achieve that, I would also greatly appreciate it. (And by the way, I do have to use a named list, I don't think I can use ...).

Thank you so much for your help.

Jesse Kerr
  • 341
  • 1
  • 8

3 Answers3

3

You can use rlang::inject():

inject(cbind(!!!letters))
Lionel Henry
  • 6,652
  • 27
  • 33
  • Is there a way to also include an unnamed parameter in the list given to blast, or include another parameter separately? I.e., if my function has 3 parameters, and I want to pipe in the first one, and use blast for the other 2? The code would be like `df_to_apply_to %>% blast(func_to_apply(!!!other_params))` – Jesse Kerr Feb 03 '20 at 15:51
  • `blast()` is not meant to be piped into. It has to wrap the expression within which quasiquotation should happen. So: `blast(df %>% f(!!!other))` – Lionel Henry Feb 04 '20 at 11:00
1

I think it's important to distinguish between literal values (a.k.a. constants) and unevaluated expressions. For example, quos( b=2, c=3 ) will always evaluate to 2 and 3, regardless of context. In such cases, you don't really need those to be quosures or expressions, and a simple list of values will do. You can then use purrr::lift to transform any arbitrary function from taking ... dots to taking a list. No !!! needed:

arglist <- list( replace=TRUE, size=5, x=1:10 )     # Note: list, not quos
sample2 <- purrr::lift(sample)
sample2( arglist )         # Same as sample( x=1:10, size=5, replace=TRUE)
# [1]  7  3 10  8  3

Unevaluated expressions come into play when you want to reference variables or columns that may not have been defined yet. In such cases, you can take advantage of rlang::list2() to capture argument lists spliced by !!!:

subset2 <- function( x, ... )
    rlang::eval_tidy(rlang::expr(subset( {{x}}, !!!rlang::list2(...) )))

# Capture expressions because mpg and cyl are undefined at this point
argexpr <- rlang::exprs( mpg < 15, select=cyl )

# base::subset() doesn't support !!!, but our new function does!
subset( mtcars, !!!argexpr )   # Error in !argexpr : invalid argument type
subset2( mtcars, !!!argexpr )  # Same as subset( mtcars, mpg < 15, select=cyl )
mtcars %>% subset2(!!!argexpr) # Also works with the pipe
#                     cyl
# Duster 360            8
# Cadillac Fleetwood    8
# ...

In the above, subset2() constructs a subset( x, arg1, arg2, etc. ) expression "by hand", then evaluates it. The curly-curly operator is used as a shortcut for !!enquo(x) to paste the user expression directly into the final expression, while rlang::list2() expands and splices all other arguments. By using rlang::list2() instead of base:list() we are adding support for !!! to the function as a whole.

It is also worth highlighting rlang::exec() and rlang::call2(), which are tidyverse equivalents of do.call and call from base. Both offer seamless support of argument splicing with !!!:

rlang::exec( sample, !!!arglist )
eval(rlang::call2( subset, mtcars, !!!argexpr ))

Lastly, @Moody_Mudskipper has a very nice adverbs/tags package. One of those tags adds NSE support to any arbitrary function and has full integration with %>%:

library(tags)    ## installed with devtools::install_github("moodymudskipper/tags")
using_bang$sample( !!!arglist )
using_bang$subset( mtcars, !!!argexpr )
mtcars %>% using_bang$subset( !!!argexpr )
Artem Sokolov
  • 13,196
  • 4
  • 43
  • 74
  • The `subset2()` function is very strange. I don't understand what it is trying to do. – Lionel Henry Feb 04 '20 at 11:02
  • Hi @LionelHenry. It's basically the same trick as [what you taught me to use for aes()](https://stackoverflow.com/a/55816211/300187), except I'm using `list2()` in place of `enquos()` here, because the expressions are already captured in `argexpr`, so there is no need for another level of quoting. I'll add some additional clarification. – Artem Sokolov Feb 04 '20 at 14:50
  • As a side note, your trick has been useful for "fixing" base functions that use NSE to work with `map()` and `%>%`. This includes [lm](https://stackoverflow.com/a/58530100/300187), [glm](https://stackoverflow.com/a/57528229/300187), and [coxph](https://stackoverflow.com/a/57516867/300187). – Artem Sokolov Feb 04 '20 at 15:04
  • 1
    I see, the `list2()` enables `!!!` inside `...` and the `blast()` on the outside (or eval + expr in this case) forwards the arguments to `subset()`. I wonder if we could make that pattern easier. By the way, I think it's better to reproduce as many named arguments as possible in `subset2()`'s signature. – Lionel Henry Feb 05 '20 at 08:49
  • @LionelHenry: Yes, you're right. I tried to keep it simple, since my answer was getting kinda long already. But I think it's important to retain `x` in this case, especially since the OP is wanting to use `%>%`. – Artem Sokolov Feb 05 '20 at 15:10
0

You could use match.call() inside your function to get the list of arguments and their names:

myfun <- function(x, ...){
    args <- as.list(match.call())[-1]
    print(setNames(unlist(args), names(args)))
    lapply(match.call()[-1], class)
}

myfun(x=list(a=1, b="hi", c="a"), b=5)
#> $x
#> list(a = 1, b = "hi", c = "a")
#> 
#> $b
#> [1] 5
#> $x
#> [1] "call"
#> 
#> $b
#> [1] "numeric"
user12728748
  • 8,106
  • 2
  • 9
  • 14