Conditional pipelines

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