Skip to content

[R-pkg-devel] NOTE about use of `:::`

7 messages · David Kepplinger, Simon Urbanek, Bill Dunlap +2 more

#
Dear List,

I am working on updating the pense package and refactored some of the
methods. I have several functions which take the same arguments, hence I'm
sending all these arguments to an internal function, called `parse_args()`.
Since I want to evaluate the arguments in the caller's environment, I'm
using the following code

  call <- match.call(expand.dots = TRUE)
  call[[1]] <- quote(pense:::parse_args)
  args <- eval.parent(call)

Of course, R CMD CHECK complains about the use of `:::`, as it's almost
never needed. I think the above usage would fall into that area of
"almost", but I'm not sure if (a) there's a better approach and (b) the
CRAN team would agree with me. I would have to test (b) by submitting and
working with the CRAN team, but I wanted to ask the list first to see if
I'm missing something obvious. I don't want to export the function
parse_args() as it's not useful for a user, and the use is truly internal.

Thanks and all the best,
David
#
David,

why not

call[[1]] <- parse_args

The assignment is evaluated in your namespace so that makes sure the call is that of your function. The only downside I see is that in a stack trace you'll see the definition instead of the name.
Or possibly

do.call(parse_args, as.list(call[-1]))

Cheers,
Simon

PS: Note that ::: is expensive - it probably doesn't matter here, but would in repeatedly called functions.
#
Thank you both for the suggestions. I would prefer a clean stack trace in
case of errors as input errors are caught by this function and hence users
might very well see errors emitted from it. It seems more informative to me
if the error message would say "Error in .parse_args?" than "Error in
new.env(?". But since this solution was suggested by both of you it is
likely that CRAN too would demand this or a similar approach instead of
using `:::`. I know `:::` is expansive, but the computations that follow
are typically at least a few minutes so anything that takes less than a few
seconds won't be noticeable.

I also thought about using `...` before, but then the signature of the
user-facing functions would be incomplete and IDEs won't be able to provide
suggestions.

Thanks for the help!

-- David

On Wed, Dec 14, 2022 at 7:11 PM Simon Urbanek <simon.urbanek at r-project.org>
wrote:

  
  
#
You could add an 'envir' argument to parse_args() and do your eval(...,
envir=envir) stuff inside parse_args().  Then change
  call[[1]] <- quote(pense:::parse_args)
  args <- eval.parent(call)
to
   call[[1]] <- quote(parse_args)
   call$envir <- parent.frame()
   args <- eval(call)
[That code is completely untested.]

-Bill

On Wed, Dec 14, 2022 at 3:19 PM David Kepplinger <david.kepplinger at gmail.com>
wrote:

  
  
#
Here's another suggestion, not sure if it's any good, but you could
structure your functions like

parse_args <- function (envir = parent.frame())
{
    evalq(list(a = a, b = b, ..., y = y, z = z), envir)
    <...>
}

exported_fun <- function (a, b, ..., y, z)
{
    parse_args()
    <...>
}

It's seriously ugly, but it could work. You could also do some bquote
substitution

parse_args_expr <- quote(parse_args(a = a, b = b, ..., y = y, z = z))

exported_fun <- function (a, b, ..., y, z) NULL
body(exported_fun) <- bquote({
   .(parse_args_expr)
    <...>
})

On Wed, Dec 14, 2022, 20:36 David Kepplinger <david.kepplinger at gmail.com>
wrote:

  
  
#
If you want the name of the function to appear, then you can put the 
function in an environment whose parent is where most of the evaluation 
should take place.  For example,

f <- function(...) {
   call <- match.call(expand.dots = TRUE)
   call[[1]] <- quote(parse_args)
   envir <- new.env(parent = parent.frame())
   envir$parse_args <- parse_args

   eval(call, envir)
}

parse_args <- function(...) {
   cat("args were ", names(list(...)), "\n")
   stop("Error in parse_args")
}

f(a = 1, b = 2)
#> args were  a b
#> Error in parse_args(a = 1, b = 2): Error in parse_args

Duncan Murdoch
On 14/12/2022 8:35 p.m., David Kepplinger wrote:
4 days later
#
I think I will go with the suggestion of creating a new empty environment,
adding only the argument parsing function. Moreover I will use the same
name as the user-facing function that's being invoked,  i.e.,

foo <- function(...) {
   call <- match.call(expand.dots = TRUE)
   call[[1]] <- quote(parse_args)
   envir <- new.env(parent = parent.frame())
   envir$foo <- parse_args

   eval(call, envir)
}

This way a trace-back shows the function the user expects, without having
to export the parsing function or duplicate the code in the user-facing
functions.

Thanks again everyone for the suggestions.

All the best,
David


On Thu, Dec 15, 2022 at 4:39 AM Duncan Murdoch <murdoch.duncan at gmail.com>
wrote: