18

Related to this question.

I'd like to build a custom pipe %W>% that would silence warnings for one operation

library(magrittr)
data.frame(a= c(1,-1)) %W>% mutate(a=sqrt(a)) %>% cos

will be equivalent to :

w <- options()$warn
data.frame(a= c(1,-1)) %T>% {options(warn=-1)} %>%
  mutate(a=sqrt(a))    %T>% {options(warn=w)}  %>%
  cos

These two tries don't work :

`%W>%` <- function(lhs,rhs){
  w <- options()$warn
  on.exit(options(warn=w))
  options(warn=-1)
  lhs %>% rhs
}

`%W>%` <- function(lhs,rhs){
  lhs <- quo(lhs)
  rhs <- quo(rhs)
  w <- options()$warn
  on.exit(options(warn=w))
  options(warn=-1)
  (!!lhs) %>% (!!rhs)
}

How can I rlang this into something that works ?

moodymudskipper
  • 46,417
  • 11
  • 121
  • 167
  • 5
    Upvote mostly for the use of `rlang` as a verb. – crazybilly Nov 29 '17 at 21:50
  • 2
    You might want to have a look at the [`rmonad::`](https://cran.r-project.org/web//packages/rmonad/) package [intro vignette](https://cran.r-project.org/web//packages/rmonad/vignettes/introduction.html) (and the others). It is a nice way of handling errors, and would probably work just as well for warnings. Possibly overkill, but something to consider. – lefft Nov 30 '17 at 22:24
  • Very interesting indeed. It might even contain the answer to this old question of mine : https://stackoverflow.com/questions/44831342/use-multiple-command-chains-with-piping – moodymudskipper Nov 30 '17 at 22:50

4 Answers4

9

I think I would approach it like this, by tweaking the magrittr pipes to include this new option. This way should be pretty robust.

First we need to insert a new option into magrittr's function is_pipe by which it is determined whether a certain function is a pipe. We need it to recognise %W>%

new_is_pipe = function (pipe)
{
  identical(pipe, quote(`%>%`)) || identical(pipe, quote(`%T>%`)) ||
    identical(pipe, quote(`%W>%`)) ||
    identical(pipe, quote(`%<>%`)) || identical(pipe, quote(`%$%`))
}
assignInNamespace("is_pipe", new_is_pipe, ns="magrittr", pos="package:magrittr")
`%W>%` = magrittr::`%>%`

We also need a new helper function that checks whether the pipe being processed is a %W>%

is_W = function(pipe) identical(pipe, quote(`%W>%`))
environment(is_W) = asNamespace('magrittr')

Finally, we need to put a new branch into magrittr:::wrap_function which checks if this is a %W>% pipe. If so, it inserts options(warn = -1) and on.exit(options(warn = w) into the body of the function call.

new_wrap_function = function (body, pipe, env)
{
  w <- options()$warn
  if (magrittr:::is_tee(pipe)) {
    body <- call("{", body, quote(.))
  }
  else if (magrittr:::is_dollar(pipe)) {
    body <- substitute(with(., b), list(b = body))
  }
  else if (is_W(pipe)) {
    body <- as.call(c(as.name("{"), expression(options(warn=-1)), parse(text=paste0('on.exit(options(warn=', w, '))')), body))
  }
  eval(call("function", as.pairlist(alist(. = )), body), env, env)
}
assignInNamespace("wrap_function", new_wrap_function, ns="magrittr", pos="package:magrittr")

Testing this works:

data.frame(a= c(1,-1)) %W>% mutate(a=sqrt(a)) %>% cos
#           a
# 1 0.5403023
# 2       NaN

compared to...

data.frame(a= c(1,-1)) %>% mutate(a=sqrt(a)) %>% cos
#           a
# 1 0.5403023
# 2       NaN
# Warning message:
# In sqrt(a) : NaNs produced
dww
  • 30,425
  • 5
  • 68
  • 111
  • Wow this is really cool! Great info to understand the gut of `maggritr`! My only concern with this is how to load it, whereas I could just put the other solutions in a script I source along with my other custom functions this one had to access `magrittr` namespace. should I load it only after I load `magrittr` ? How would I put this functions in a package ? – moodymudskipper Nov 28 '17 at 18:12
  • I don't think it matters whether you run this code before or after loading magrittr. Although I can't think of any particular reason to do it first, given that you need to load the package to run these functions. Also, I can't think of any snafus from putting this into a package. I'll give it some more thought later, but please let me know if you come across any. – dww Nov 28 '17 at 18:24
  • I will do. I'll probably package this during the week. – moodymudskipper Nov 28 '17 at 19:22
  • Actually I'm still confused, if I want to put this in a package, what should I do with you `assignInNamespace` and `environment` instructions ? so far I've only put regular functions in packages, they should be set as instructions to run when `library` is called ? I can post this as a separate question if it's too broad – moodymudskipper Dec 03 '17 at 15:32
  • Maybe this helps? https://stackoverflow.com/questions/4019049/run-code-first-time-a-package-is-installed-or-used – dww Dec 04 '17 at 04:49
  • Yes thanks, it seems (untested) that I'll be fine by in a R file traditionally named `zzz.R` a function `.onLoad <- function(libname, pkgname) {the_3_instructions }` – moodymudskipper Dec 04 '17 at 08:41
  • @Moody_Mudskipper if you did made a package out of it, I'd be extremely interested to see how you did. Is there any chance you made a gist or a github out of this ? – Dan Chaltiel Oct 22 '18 at 12:46
  • @DanChaltiel I wrote a package with a set of pipe operators, following dww's answer, you can check it there : https://github.com/moodymudskipper/mmpipe , see also my answer below. – moodymudskipper Nov 17 '18 at 03:52
3

I'm not sure this solution works perfectly, but it's a start:

`%W>%` <- function(lhs, rhs) {
  call <- substitute(`%>%`(lhs, rhs))
  eval(withr::with_options(c("warn" = -1), eval(call)), parent.frame())
}

This seems to work for the following 2 examples:

> data.frame(a= c(1,-1)) %W>% mutate(a=sqrt(a)) %>% cos
          a
1 0.5403023
2       NaN
> c(1,-1) %W>% sqrt()
[1]   1 NaN
F. Privé
  • 11,423
  • 2
  • 27
  • 78
3

Perhaps something like this with rlang:

library(rlang)
library(magrittr)

`%W>%` <- function(lhs, rhs){
  w <- options()$warn
  on.exit(options(warn=w))
  options(warn=-1)
  lhs_quo = quo_name(enquo(lhs))
  rhs_quo = quo_name(enquo(rhs))
  pipe = paste(lhs_quo, "%>%", rhs_quo)
  return(eval_tidy(parse_quosure(pipe)))
}

data.frame(a= c(1,-1)) %W>% mutate(a=sqrt(a)) %>% cos

Result:

          a
1 0.5403023
2       NaN

Note:

  • You need enquo instead of quo because you are quoting the code that was supplied to lhs and rhs, not the literals lhs and rhs.

  • I couldn't figure out how to feed lhs_quo/lhs into rhs_quo (which was a quosure) before it was evaluated, neither can I evaluate rhs_quo first (throws an error saying a not found in mutate(a=sqrt(a)))

  • The workaround that I came up with turns lhs and rhs into strings, pastes them with "%>%", parses the string to quosure, then finally tidy evaluates the quosure.

acylam
  • 18,231
  • 5
  • 36
  • 45
3

Coming back a little more experienced, I just missed an eval.parent and substitute combo, no need for rlang :

`%W>%` <- function(lhs,rhs){
  # `options()` changes options but returns value BEFORE change
  opts <- options(warn = -1) 
  on.exit(options(warn=opts$warn))
  eval.parent(substitute(lhs %>% rhs))
}

data.frame(a= c(1,-1)) %W>% mutate(a=sqrt(a)) %>% cos
#           a
# 1 0.5403023
# 2       NaN
moodymudskipper
  • 46,417
  • 11
  • 121
  • 167
  • May you explain what each part of `eval.parent` in the function is doing? And am I understanding correctly that `w <- options()$warn` records the current state of the warning display, `on.exit(options(warn = w))` returns the option to to this state once the function completes, and `options(warn = -1)` silences the warning during the function. – its.me.adam Feb 25 '22 at 02:23
  • 1
    Your understanding is correct, I changed slightly the code to make it more idiomatic. `substitute()` builds a piped expression, in this case `quote(data.frame(a= c(1,-1)) %>% mutate(a=sqrt(a)))`, `eval.parent()` evaluates it in the caller environment, where `data.frame(a= c(1,-1)) %>% mutate(a=sqrt(a))` would have been called if we had replaced `%W>%` with `%>%` – moodymudskipper Feb 25 '22 at 07:58
  • 1
    Try `opts <- 1; data.frame(a= c(opts,-opts)) %W>% mutate(a=sqrt(a)) %>% cos`, then replace `eval.parent()` with `eval()` and see what happens. If you don't understand it I'm happy to help further. – moodymudskipper Feb 25 '22 at 08:01
  • We get `Error in -opts: invalid argument to unary operator`, just like running `opts <- options(warn = -1); -opts`, which is what's happening. The last part I am struggling with is `substitute`. Why can't we use `quote`? I do not know what parse tree or what the second clause means in`?substitute`'s "substitute returns the parse tree for the (unevaluated) expression expr, substituting any variables bound in env." – its.me.adam Feb 27 '22 at 05:05
  • 1
    Try defining `"%W>%" <- function(lhs, rhs) substitute(lhs %>% rhs)` then `"%W>%" <- function(lhs, rhs) quote(lhs %>% rhs)` , test them both on `data.frame(a= c(1,-1)) %W>% mutate(a=sqrt(a))`. You will see that `quote()` captures the expression while `substitute()`, well, substitutes the symbols with expressions that were given as arguments – moodymudskipper Feb 27 '22 at 07:21
  • 1
    The doc from substitute is confusing and you don't need to understand what a parse tree is to use the function. Basically a call is a tree in the sense that it's hierarchical, try `lobstr::ast(data.frame(a= c(1,-1)) %W>% mutate(a=sqrt(a)))` and you'll see how the call is **parsed** before being executed. – moodymudskipper Feb 27 '22 at 07:30