When using pipleines in Shiny I find myself wanting to create conditional steps in the pipeline. I want to alter the steps of the pipleine based on the user input. I came up with one idea but I wonder if anyone else has had this problem and if there is an elegant way to handle it.
My solution:
conditionally <- function(fun){
function(first_arg, ..., execute){
if(execute) return(fun(first_arg, ...))
else return(first_arg)
}
}
library(dplyr)
cond_filter <- conditionally(filter)
cond_select <- conditionally(select)
mtcars %>%
cond_filter(cyl == 4, execute = T) %>%
cond_select(cyl, execute = F)
8 Likes
You can simplify a bit by using ..n
notation to refer to the n
th term of ...
:
library(dplyr)
conditionally <- function(fun){
function(..., execute) {
if (execute) fun(...) else ..1
}
}
mtcars %>%
conditionally(filter)(hp == 245, execute = TRUE) %>%
conditionally(select)(cyl, execute = FALSE)
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> 1 14.3 8 360 245 3.21 3.57 15.84 0 0 3 4
#> 2 13.3 8 350 245 3.73 3.84 15.41 0 0 3 4
Without writing your own adverbial function, you can do this inline with braces to control where the incoming data .
is passed:
mtcars %>%
{if (TRUE) filter(., hp == 245) else .} %>%
{if (FALSE) select(., cyl) else .}
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> 1 14.3 8 360 245 3.21 3.57 15.84 0 0 3 4
#> 2 13.3 8 350 245 3.73 3.84 15.41 0 0 3 4
With a little rlang magic, you can write a version of conditionally
that does the same thing, even taking raw expressions for the condition and call and evaluating each in the context of the data:
provided <- function(data, condition, call) {
if (rlang::eval_tidy(enquo(condition), data)) {
rlang::eval_tidy(rlang::quo_squash(quo(data %>% !!enquo(call))))
} else data
}
mtcars %>%
provided(all(cyl > 0), filter(hp == 245)) %>%
provided(any(cyl < 0), select(cyl))
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> 1 14.3 8 360 245 3.21 3.57 15.84 0 0 3 4
#> 2 13.3 8 350 245 3.73 3.84 15.41 0 0 3 4
As far as API structure, I worry that implicitly passing .
into the call
parameter is inconsistent with the pipe's expected behavior, but the code does read nicely.
18 Likes
Thanks for these very helpful suggestions!
Would you mind breaking down what is happening in this line of code?
I have a basic understanding of what enquo
, quo
, and !!
do but this line makes my head spin a bit. Is there an easy way you think about all this quoting and unquoting? I just learned that R code can be thought of as a tree. Maybe that would help.
Also I don't fully understand your last comment about API structure. Do you just mean that this API would fail when the call
argument was not a function that expected the data as the first argument (i.e. call
was not a pipe-able function)?
Working from the inside out, call
is some code to pipe the previous result into, so enquo
quotes it, keeping it as a language object (a quosure) so R doesn't try to evaluate it as soon as it's referred to. This is placed in another quosure made by quo
that pipes the data
piped in into the call
, unquoted with !!
to substitute the code supplied for the variable call
.
The unquoting leaves you with a nested quosure, e.g. for the example
<quosure>
expr: ^data %>% (^filter(hp == 245))
env: 0x122dcbb18
quo_squash
flattens the structure into a single expression
data %>% filter(hp == 245)
which eval_tidy
then runs.
Braces and specifying where to send the data with .
will work, but yes, the call must work when the preceding data is piped in. My point was more that it's a little weird to write filter
without a pipe immediately preceding it or the data specified inside. It's probably the only way a piped conjunction function could be expected to work, but it is upping the API complexity.
5 Likes
After some thought I think the following is the best way to implement conditional pipelines since it does not involve defining any new functions.
mtcars %>%
{if (TRUE) filter(., hp == 245) else .} %>%
{if (FALSE) select(., cyl) else .}
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> 1 14.3 8 360 245 3.21 3.57 15.84 0 0 3 4
#> 2 13.3 8 350 245 3.73 3.84 15.41 0 0 3 4
8 Likes