Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Converting positional arguments to named parameters in an R function based on variable name

Tags:

r

pryr

In R there's common function calling pattern that looks like this:

child = function(a, b, c) {
  a * b - c
}

parent = function(a, b, c) {
  result = child(a=a, b=b, c=c)
}

This repetition of names is helpful because it prevents potentially insidious errors if the ordering of the child's arguments were to change, or if an additional variable were to be added into the list:

childReordered = function(c, b, a) {  # same args in different order
  a * b - c
}

parent = function(a, b, c) {
  result = childReordered(a, b, c)  # probably an error
}

But this becomes cumbersome when the names of the arguments get long:

child = function(longVariableNameA, longVariableNameB, longVariableNameC) {
 longVariableNameA * longVariableNameB - longVariableNameC
}

parent = function(longVariableNameA, longVariableNameB, longVariableNameC) {
  child(longVariableNameA=longVariableNameA, longVariableNameB=longVariableNameB, longVariableNameC=longVariableNameB)
}

I'd like to have a way to get the safety of the named parameters without needing to actually type the names again. And I'd like to be able to do this when I can modify only the parent, and not the child. I'd envision something like this:

parent = function(a, b, c) {
  result = child(params(a, b, c))
}

Where params() would be a new function that converts the unnamed arguments to named parameters based on the names of the variables. For example:

child(params(c,b,a)) == child(a=a, b=b, c=c)

There are a couple function in 'pryr' that come close to this, but I haven't figured out how to combine them to do quite what I want. named_dots(c,b,a) returns list(c=c, b=b, a=a), and standardise_call() has a similar operation, but I haven't figured out how to be able to convert the results into something that can be passed to an unmodified child().

I'd like to be able to use a mixture of implicit and explicitly named parameters:

child(params(b=3, c=a, a)) == child(b=3, c=a, a=a)

It would also be nice to be able to mix in some unnamed constants (not variables), and have them treated as named arguments when passed to the child.

child(params(7, c, b=x)) == child(a=7, c=c, b=x)  # desired
named_dots(7, c, b=x) == list("7"=7, c=c, b=x)  # undesired

But in a non-R way, I'd prefer to raise errors rather than trying to muddle through with what are likely programmer mistakes:

child(params(c, 7, b=x)) # ERROR: unnamed parameters must be first

Are there tools that already exist to do this? Simple ways to piece together existing functions to do what I want? Better ways to accomplish the same goal of getting safety in the presence of changing parameter lists without unwieldy repetition? Improvements to my suggested syntax to make it even safer?

Pre-bounty clarification: Both the parent() and child() functions should be considered unchangeable. I'm not interested in wrapping either with a different interface. Rather, I'm looking here for a way to write the proposed params() function in a general manner that can rewrite the list of arguments on the fly so that both parent() and child() can be used directly with a safe but non-verbose syntax.

Post-bounty clarification: While inverting the parent-child relationship and using do.call() is a useful technique, it's not the one I'm looking for here. Instead, I'm looking for a way to accept a '...' argument, modify it to have named parameters, and then return it in a form that the enclosing function will accept. It's possible that as others suggest this is truly impossible. Personally, I currently think it is possible with a C level extension, and my hope is that this extension already exists. Perhaps the vadr package does what I want? https://github.com/crowding/vadr#dot-dot-dot-lists-and-missing-values

Partial credit: I feel silly just letting the bounty expire. If there are no full solutions, I'll award it to anyone who gives a proof of concept of at least one of the necessary steps. For example, modifying a '...' argument within a function and then passing it to another function without using do.call(). Or returning an unmodified '...' argument in a way that the parent can use it. Or anything that best points the way toward a direct solution, or even some useful links: http://r.789695.n4.nabble.com/internal-manipulation-of-td4682090.html But I'm reluctant to award it to an answer that starts with the (otherwise entirely reasonable) premise that "you don't want to do that", or "that's impossible so here's an alternative".

Bounty awarded: There are several really useful and practical answers, but I chose to award the bounty to @crowding. While he (probably correctly) asserts that what I want is impossible, I think his answer comes closest to the 'idealistic' approach I'm aiming for. I also think that his vadr package might be a good starting point for a solution, whether it matches my (potentially unrealistic) design goals or not. The 'accepted answer' is still up for grabs if in case someone figures out a way to do the impossible. Thanks for the other answers and suggestions, and hopefully they will help someone put together the pieces for a more robust R syntax.

like image 920
Nathan Kurz Avatar asked Aug 22 '15 21:08

Nathan Kurz


People also ask

When using a function the functions arguments can be specified by?

Because all function arguments have names, they can be specified using their name. Specifying an argument by its name is sometimes useful if a function has many arguments and it may not always be clear which argument is being specified. Here, our function only has one argument so there's no confusion.

Can parameters and arguments have the same name?

Show activity on this post. Yes, the names of the variables you pass in a function call can be the same as the names of the parameters in the function definition.


1 Answers

I think attempting to overwrite the built argument matching functionality of R is somewhat dangerous, so here is a solution that uses do.call.

It was unclear how much of parent is changeable

# This ensures that only named arguments to formals get passed through
parent = function(a, b, c) {
   do.call("child", mget(names(formals(child))))
}

A second option, based on the "magic" of write.csv

# this second option replaces the call to parent with child and passes the 
# named arguments that have been matched within the call to parent
# 

parent2 <- function(a,b,c){
  Call <- match.call()
  Call[[1]] <- quote(child)
  eval(Call)
}
like image 70
mnel Avatar answered Sep 28 '22 10:09

mnel