10

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.

Community
  • 1
  • 1
Nathan Kurz
  • 1,649
  • 1
  • 14
  • 28
  • `do.call` and `match.call` don't do all you want, but I think having a closer look at these functions might be interesting for you. By the way, your question reminds my of [a question I asked](http://stackoverflow.com/questions/29327423/use-match-call-to-pass-all-arguments-to-other-function) some time ago, unfortunately unanswered. – CL. Aug 22 '15 at 22:47
  • It would help if you provided some test case to verify possible solutions. – MrFlick Aug 31 '15 at 18:16
  • A solution would be any function params() such that child(params(c,b,a)) always evaluates identically to child(c=c, b=b, a=a). Both you and mnel offer practical solutions using wrappers, which hopefully will be useful to others who are able to to use do.call() syntax. But for the purposes of this question and the bounty, I'm looking only for a way to write a function that accepts a multiparameter '...' argument, modifies it to have named parameters, and returns it in a way that a parent function will accept this single argument in lieu of the multiple arguments it would normally receive. – Nathan Kurz Sep 01 '15 at 18:54
  • @NathanKurz I can get your last comment, you'll still have to modify each call to those functions to add the new one inside, so refactoring to use the wrapper is not more complicated than what you ask for... (and the do.call method does not invert the relationship) – Tensibai Sep 02 '15 at 16:04
  • Curious how @Mnel's answer, and my expounding thereon, fail to address your question. I guess it could be the "both `parent` and `child` should be considered unchangeable", but in your own example you change `parent` by using `child(params(...))`. I see this as a change of the same magnitude as say, `params(child)` which is basically what mnel's / my answer do. – BrodieG Sep 03 '15 at 20:23
  • The easy answer might be that I started out with a do.call() solution very similar to mnel's, and posted the question because it seemed like there should be a better way. I felt like I understood his solution coming in, although it's also possible I'm just not seeing it correctly. I read your expansion soon after you posted it, but haven't worked it up as code yet. I think I will have time tomorrow, and will report back. – Nathan Kurz Sep 04 '15 at 04:17

6 Answers6

6

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)
}
mnel
  • 113,303
  • 27
  • 265
  • 254
  • Maybe to make it clearer to OP, reformulate `parent2` to something like `call_as_parent(child)` then with `match.call(call=sys.call(sys.parent())` you can have `parent <- function(a, b, c) call_as_parent(child)` (+1) – BrodieG Sep 03 '15 at 13:20
4

You can't change the parameters to a function from inside the function call. The next best way would be to write a simple wrapper around the call. Perhaps something like this can help

with_params <- function(f, ...) {
    dots <- substitute(...())
    dots <- setNames(dots, sapply(dots, deparse))
    do.call(f, as.list(dots), envir=parent.frame())
}

And we can test with something like

parent1 <- function(a, b, c) {
  child(a, b, c)
}

parent2 <- function(a, b, c) {
  with_params(child, a, b, c)
}

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

parent1(5,6,7)
# [1] 23
parent2(5,6,7)
# [1] 23

child <- function(c, a, b) {
  a * b - c
}
parent1(5,6,7)
# [1] 37
parent2(5,6,7)
# [1] 23

Note that parent2 is robust to change in the parameter order of the child while parent is.

MrFlick
  • 195,160
  • 17
  • 277
  • 295
4

It's not easy to get the exact syntax you're proposing. R is lazily evaluated, so syntax appearing in an argument to a function is only looked at after the function call is started. By the time the interpreter encounters params() it will have already started the call to child() and bound all of child's arguments and executed some of child's code. Can't rewrite child's arguments after the horse has left the barn, so to speak.

(Most non-lazy languages wouldn't let you do this either, for various reasons)

So the syntax will need to be something that that has the the references to 'child' and 'params' in its arguments. vadr has a %()% operator that can work. It applies a dotlist given on the right to a function given on the left. So you would write:

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

where params catches its arguments in a dotlist and manipulates them:

params <- function(...) {
  d <- dots(...)
  ex <- expressions(d)
  nm <- names(d) %||% rep("", length(d))
  for (i in 1:length(d)) {
    if (is.name(ex[[i]]) && nm[[i]] == "") {
      nm[[i]] = as.character(ex[[i]])
    }
  }
  names(d) <- nm
  d
}
crowding
  • 1,498
  • 10
  • 11
1

This was meant to be a comment but it doesn't fit the limits. I commend you for your programming ambition and purity, but I think the goal is unattainable. Let's assume params exists and apply it to the function list. By the definition of params, identical(list(a = a, b = b, c = c) , list(params(a, b, c)). From this it follows that identical(a, params(a, b, c)) by taking the first element of the first and second argument of identical. From which it follows that params does not depend on its second and later arguments, a contradiction. Q.E.D. But I think your idea is a lovely example of DRY in R and I am perfectly happy with do.call(f, params(a,b,c)), which has an additional do.call, but no repetition. With your permission I would like to incorporate it in my package bettR which collects various ideas to improve the R language. A related idea which I was toying with is creating a function that allows another function to get missing args from the calling frame. That is, instead of calling f(a = a, b = b), one could call f() and inside f there would be something like args = eval(formals(f), parent.frame()) but encapsulated into a macro-like construct args = get.args.by.name or some such. This is distinct from your idea in that it requires f to be programmed deliberately to have this feature.

piccolbo
  • 1,305
  • 7
  • 17
  • I don't follow your "From this it follows...". Could you fill in some of the steps of your reasoning? Yes, definitely use this however you see fit. And also check out crowding's vadr package. I hadn't seen it before researching this question, but I think you'd like his approach. – Nathan Kurz Sep 03 '15 at 09:44
  • I am just using a property of the `list` function such that `list(x1, x2, ... , xn)[[1]]` is identical to `x1`. So going back to your definition of `params`, `identical(list(a = a, b = b, c = c) , list(params(a, b, c))` we take the first element left and right `identical(list(a = a, b = b, c = c) [[1]], list(params(a, b, c))[[1]]` because if two lists are identical, so are their first elements. Extract the first elements and you have `identical(a, params(a, b, c))`. Of course these are not mathematical objects and you could imagine a `params` so powerful that it breaks `list` – piccolbo Sep 03 '15 at 15:24
  • ... but it seems a very long shot. – piccolbo Sep 03 '15 at 15:28
  • Got it. List is a somewhat overloaded term in R, and params() would not be returning a list of the same sort returned by list(). Instead, it would need to create and return an object of the same type as '...', which is a DOTSXP, which in turn is a special variant of a pairlist of promises: https://cran.r-project.org/doc/manuals/r-release/R-ints.html#Dot_002ddot_002ddot-arguments – Nathan Kurz Sep 03 '15 at 17:00
  • Never assumed params would return a list. I see your point though `!identical(list(...)[[1]], ...)` whatever that means. So how do we generate a DOTSXP? – piccolbo Sep 03 '15 at 18:31
1

Here's an answer that at first would appear to work. Diagnosing why it does not may lead to enlightenment as to why it cannot (hint: see first paragraph of @crowding's answer).

params<-function(...) {
  dots<-list(...)
  names(dots)<-eval(substitute(alist(...)))
  child.env<-sys.frame(-1)  
  child.fun<-sys.function(sys.parent()+1)
  args<-names(formals(child.fun))
  for(arg in args) {
    assign(arg,dots[[arg]],envir=child.env)
  }
  dots[[args[[1]]]]
}

child1<-function(a,b,c) a*b-c
parent1<-function(a,b,c) child1(params(a,b,c))
parent1(1,2,3)
#> -1

child2<-function(a,c,b) a*b-c #swap b and c in formals
parent2<-function(a,b,c) child2(params(a,b,c)) #mirrors parent1
parent2(1,2,3)
#> -1

Both produce 1*2-3 == -1 even though the order of b=2 and c=3 have been swapped in the formal argument list of child1 versus child2.

Community
  • 1
  • 1
A. Webb
  • 26,227
  • 1
  • 63
  • 95
1

This is basically a clarification to Mnel's answer. If it happens to answer your question, please do not accept it or award the bounty; it should go to Mnel. First, we define call_as_parent, which you use to call a function inside another function as the outside function:

call_as_parent <- function(fun) {
  par.call <- sys.call(sys.parent())
  new.call <- match.call(
    eval(par.call[[1L]], parent.frame(2L)), 
    call=par.call
  )
  new.call[[1L]] <- fun
  eval(new.call, parent.frame())
}

Then we define parent and child:

child <- function(c, b, a) a - b - c
parent <- function(a, b, c) call_as_parent(child)

And finally, some examples

child(5, 10, 20)
# [1] 5
parent(5, 10, 20)
# [1] -25

Notice how clearly in the second example the 20 is getting matched to c, as it should.

BrodieG
  • 51,669
  • 9
  • 93
  • 146