14

I would like to be able to print the name of a dataframe passed through the pipe. Is this possible? I can do.

printname <- function(df){
    print(paste(substitute(df)))
}
printname(mtcars)
#[1] "mtcars"

However, it returns "." when this function is piped using the magrittr pipe.

mtcars %>% printname
# [1] "."

This would be helpful when writing custom error messages of functions used in logged production processes -- it's hard to know where something failed if the only thing in the log is "."

It would probably be enough to return the original call, which would include the mtcars %>% piece.

Ryan Knight
  • 1,288
  • 1
  • 11
  • 18
  • How do you start a pipeline without knowing the name of the data.frame? `get`? There's some missing context here. – alistaire Mar 02 '17 at 17:51
  • You know the name when you write the code, but the functions you call through the pipe do not know the name, so they can't raise informative errors. You want runtime assertions to raise errors that contain the name of the dataframe that caused the error. – Ryan Knight Mar 03 '17 at 16:17
  • Fair. Line numbers would be nice too, now that I think about it. – alistaire Mar 03 '17 at 16:29

2 Answers2

14

This is a first attempt, it's kind of a hack, but seems like it might work.

find_chain_parts <- function() {
    i <- 1
    while(!("chain_parts" %in% ls(envir=parent.frame(i))) && i < sys.nframe()) {
          i <- i+1
      }
    parent.frame(i)
}

printfirstname <- function(df){
    ee <- find_chain_parts()
    print(deparse(ee$lhs))
}

mtcars %>% printfirstname
# [1] "mtcars"

The pipe function creates an environment that keeps track of the chain parts. I tried walking up the current execution environments looking for this variable and then use the lhs info stored there to find the symbol at the start of the pipe. This isn't well tested.

MrFlick
  • 195,160
  • 17
  • 277
  • 295
  • 1
    This worked nicely, but is now returning NULL for me on R 4.0.3, dplyr 1.0.2 for all steps in the while(). – Tom Dec 08 '20 at 11:19
  • 2
    This trick was using internal implementation details of magrittr 1.5 and no longer works with magrittr 2.0. – Lionel Henry Feb 01 '21 at 09:20
  • @LionelHenry So is there an alternative in 2.0? Or is there some more official support for navigating the chain from within? – MrFlick Feb 01 '21 at 20:08
  • You could use `magrittr::pipe_nested()` as a pipe, this uses the same rewrite approach as the `|>` pipe of R 4.1. So `foo %>% f()` is rewritten as `f(foo)` before evaluation and the name can be captured. I'd still avoid any pattern that relies on this though. – Lionel Henry Feb 02 '21 at 09:58
2

As Tom & Lionel Henry commented on MrFlick's answer, the accepted answer no long works under more magrittr 2.

A new answer, then, eschews deparse(substitute()) for sys.calls(). I get this from Artem Sokolov's answer here. I won't pretend to fully understand what's happening but it works for me:

x_expression <- function(x) {
  getAST <- function(ee) purrr::map_if(as.list(ee), is.call, getAST)

  sc <- sys.calls()
  ASTs <- purrr::map( as.list(sc), getAST ) %>%
    purrr::keep( ~identical(.[[1]], quote(`%>%`)) )  # Match first element to %>%

  if( length(ASTs) == 0 ) return( enexpr(x) )        # Not in a pipe
  dplyr::last( ASTs )[[2]]    # Second element is the left-hand side
}

which gives the desired output, for both pipe and non-piped notation:

x_expression(mtcars)
# mtcars

mtcars %>% x_expression()
# mtcars
Unrelated
  • 347
  • 2
  • 14