tidyverse / magrittr

Improve the readability of R code with the pipe
https://magrittr.tidyverse.org
Other
961 stars 157 forks source link

Debug pipe? #133

Closed Deleetdk closed 6 years ago

Deleetdk commented 7 years ago

Pipes are great, but debugging pipes is difficult.

Often I have a pipeline like this:

> 1 %>%
  #somewhere, an error happens
  divide_by("2")
Error in divide_by(., "2") : non-numeric argument to binary operator

It is not always be easy to figure out where the error happens.

The base-r try approach is fairly verbose:

#base r try approach
.trial = try({
  1 %>%
    divide_by("2")
})
if(.trial %>% inherits("try-error")) browser()

But it gets you what you need to find the error: the browser call in the environment where the first pipe is called from.

I wonder if one can make a pipe approach. First attempt:

> browse_on_error = function(x) {
    .trial = try({x})
    if (inherits(x, "try-error")) eval(browser, envir = parent.frame())
  }
> 
> 1 %>%
    divide_by("2") %>% 
    browse_on_error
Error in divide_by(., "2") : non-numeric argument to binary operator

Does not work because the error does not get forwarded down the pipe to the error catcher.

So the error catching must happen earlier in the pipeline. I can think of two approaches::

  1. Special pipe, maybe %E>%, E for error, analogous to %T>%
  2. Special function, maybe try_pipe().

The first would be less verbose, so presumably preferable. It would do something like the following:

#code like
1 %E>% 
  divide_by("2")

#would get rewritten to
.trial = try({
  1 %>% 
    divide_by("2")
})
if (.trial %>% inherits("try-error")) browser()

I tried unsuccessfully to implement it. Here's my broken code. It fails due to the missing argument in the call to rhs.

`%E>%` = function (lhs, rhs) {
  #try
  .trial = try({
    y = lhs %>% 
      rhs
  })

  #catch
  if (inherits(.trial, "try-error")) eval(quote(browser()), parent.frame(n = 1))

  y
}

So for now I'm stuck to using something like this:

try2 = function(expr) {
  #try
  .trial = try({
    y = eval(substitute(expr), parent.frame())
  })

  #catch
  if (inherits(.trial, "try-error")) eval(quote(browser()), parent.frame(n = 1))

  y
}

With that one can write code like this:

#a deeper error
some_func2 = function() {
  some_var1 = 1

  try2({
    1 %>%
      multiply_by(10) %>% 
      multiply_by(10) %>% 
      multiply_by(10) %>% 
      multiply_by(10) %>% 
      multiply_by(10) %>% 
      divide_by("2")  
  })
}

some_func2()

And browser is correctly called from within some_func2 so one has the necessary context to figure out the problem.

The debug pipe would have to be special. It must not only pass lhs to rhs, but evaluate all following pipes. Otherwise, it would only catch an error in the immediately following pipe and not 5 steps further down. So, like %<>%, it would always have to make sure it is the first pipe in the pipeline. Since %<>% can do this, it seems possible that one could make a debug pipe too.

Am I missing something obvious here?

pgensler commented 7 years ago

This seems to work for debugging(see link), but I think it would be nice to either have debug_pipe either use this operator for debugging(find and replace %>% with ->.;) so you can easily step through the sequence, or some other method to easily step through pipe sequences. http://www.win-vector.com/blog/2016/12/magrittrs-doppelganger/

hadley commented 6 years ago

I would rather figure out how to get browser() working; I think that's possible now - see #172 for progress.