Skip to content

Expanding partial names

12 messages · Gabor Grothendieck, Charles Dupont, Hadley Wickham +2 more

#
I'm writing wrappers for some functions that change some of the default 
arguments.  I'd rather not list all of the arguments for the low level 
functions because there are about a dozen wrapper functions, and about 
20 arguments to lowlevel.  Instead I'm trying something like this:

lowlevel <- function(longname = 1) {
   cat("longname = ", longname, "\n")
}

wrapper <- function(...) {
   newargs <- list(longname = 2)
   newargs[names(list(...))] <- list(...)
   do.call("lowlevel", newargs)
}

This almost works:

 > wrapper()
longname =  2
 > wrapper(longname = 3)
longname =  3

But it fails if I try to use partial argument matching:

 > wrapper(long=4)
Error in lowlevel(longname = 2, long = 4) :
         unused argument(s) (long ...)

because long isn't matched to longname.  Is there a reasonable way to do 
this (e.g. using pmatch or charmatch) other than listing all the low 
level arguments in the argument list to wrapper?

Duncan Murdoch
#
Try this:


wrapper <- function(...) {
  args <- list(...)
  if (length(args)) {
	  nf <- names(formals(lowlevel))
	  nams <- nf[pmatch(names(args), nf)]
	  args <- replace(list(longname = 2), nams, args)
  }
  do.call("lowlevel", args)
}

Here is a test:
longname =  1
longname =  34
longname =  34
On 3/7/06, Duncan Murdoch <murdoch at stats.uwo.ca> wrote:
#
On 3/7/2006 9:42 AM, Gabor Grothendieck wrote:
Thanks, that's getting close, but it doesn't quite handle errors cleanly:

 > wrapper(junk=3)
Error in lowlevel(longname = 2, "NA" = 3) :
         unused argument(s) (NA ...)

It looks like I'll need something fairly elaborate.

Duncan Murdoch
#
The original code was not intended to be fully finished.
It was just to give the idea so I left out the error checking.
Adding such a check is just a matter of adding an if
statement to check the pmatch for NA:

wrapper <- function(...) {
  args <- list(...)
  if (length(args)) {
     nf <- names(formals(lowlevel))
     idx <- pmatch(names(args), nf)
     if (any(is.na(idx)))
        stop(paste("Invalid names used:", names(args)[is.na(idx)]))
     nams <- nf[idx]
     args <- replace(list(longname = 2), nams, args)
  }
  do.call("lowlevel", args)
}

wrapper(long = 3)
wrapper(junk = 5)
On 3/7/06, Duncan Murdoch <murdoch at stats.uwo.ca> wrote:
#
Duncan Murdoch wrote:
If all you are doing is changing the default values of some arguments
this should work.

wrapper <- lowlevel
formals(wrapper) <- replace(formals(lowlevel), c("longname"), list(2))

Charles
#
Here's a slightly different approach:

lowlevel <- function(longname = 1, ...) {
  cat("longname = ", longname, "\n")
}

wrapper <- function(...) {
  newargs <- defaults(list(...), list(longname = 2))
  do.call("lowlevel", newargs)
}

defaults <- function(x, defaults)  {
	if (length(x) == 0) return(defaults)
	names(x) <- ifelse(is.na(pmatch(names(x), names(defaults))),
names(x), names(defaults))
	c(x, defaults[setdiff(names(defaults), names(x))])
}

wrapper()
wrapper(longname=20)
wrapper(long=20)
wrapper(junk=3)


Hadley
#
On 3/7/2006 12:08 PM, Charles Dupont wrote:
Thanks for the suggestion, but the calculation of the new defaults is 
more involved than my example indicated.  I really need to do some 
computation within the wrapper to come up with the new defaults.

Duncan Murdoch
#
On 3/7/06, Duncan Murdoch <murdoch at stats.uwo.ca> wrote:
One trick I often use that is different from any of the suggestions I have seen so far (and is more transparent IMO) is the following:


lowlevel <- function(longname = 1) {
   cat("longname = ", longname, "\n")
}

wrapper <- function(...) {
    newArgs <-
        function(longname = 2, ...)
            list(longname = longname,
                 ...)
    do.call("lowlevel", newArgs(...))
}

which gives:
longname =  2
longname =  3
longname =  20
Error in lowlevel(longname = 2, junk = 3) : 
	unused argument(s) (junk ...)

-Deepayan
#
On 3/7/2006 2:00 PM, Deepayan Sarkar wrote:
Thanks, this is a nice idea.  It looks as though it would combine well 
with Charles Duponts' suggestion to change the formals, i.e. I could 
have a generic version of your newArgs function, then change the formals 
and the body to match the pattern you used.

One thing I'd like is to be able to put the new defaults in a list, 
because this code is going to show up in about a dozen places, and I 
don't want to have to edit all of them when the arg list of the low 
level function changes.  So really I want something like

newArgs(..., newDefaults)

where newDefaults is a tagged list (e.g. list(longname = 2) for the 
example below), and the return value is a list containing all the names 
in newDefaults, perhaps with their values modified according to the args 
passed in ... .

In the actual use newDefaults would be the result of a function call 
(the user will have made some configuration choices and I want those to 
be used as defaults to another function) but that's not so important 
here.  I'd like the wrapper to be a bit like par(), though I notice that 
par() doesn't accept partial name matching so maybe I'm worrying about 
something I shouldn't.

Duncan Murdoch
#
Okay, here's my effort based on Deepayan's and Charles' ideas.  The 
newArgs function is not what I'd call transparent, but I like the way 
the wrapper looks.

 > newArgs <- function(..., Params) {
+   f <- function(...) list(...)
+   formals(f) <- c(Params, formals(f))

+   b <- as.list(body(f))
+   body(f) <- as.call(c(b[1], names, b[-1]))
+   f(...)
+ }
 >
 > lowlevel <- function(longname = 1) {
+   cat("longname = ", longname, "\n")
+ }
 >
 > newDefaults <- list(longname=2)
 >
 > wrapper <- function (...)
+   do.call("lowlevel", newArgs(..., Params=newDefaults))

newArgs sets up f to look like

function (longname = 2, ...) list(longname = longname, ...)

and then calls it.  The thing I like about this, as opposed to using 
pmatch, is that I'm sure the partial matching is what's used by R's 
argument matching, whereas that's only pretty likely with pmatch.

I also sort of like these lines:

+   names <- as.list(names(Params))
+   names(names) <- names
+   names <- lapply(names, as.name)

but maybe I should have named Params as names, so they looked like this:

+   names <- as.list(names(names))
+   names(names) <- names
+   names <- lapply(names, as.name)

And of course I like the fact that this seems to work, but we've seen 
several versions that do that:

 > wrapper()
longname =  2
 > wrapper(longname=3)
longname =  3
 > wrapper(long=3)
longname =  3
 > wrapper(long=20)
longname =  20
 > wrapper(junk=20)
Error in lowlevel(longname = 2, junk = 20) :
         unused argument(s) (junk ...)

Duncan Murdoch
#
Whoops, just noticed that I cut when I should have copied.  The newArgs 
function should look like this:

newArgs <- function(..., Params) {
   f <- function(...) list(...)
   formals(f) <- c(Params, formals(f))
   names <- as.list(names(Params))
   names(names) <- names
   names <- lapply(names, as.name)
   b <- as.list(body(f))
   body(f) <- as.call(c(b[1], names, b[-1]))
   f(...)
}

Duncan Murdoch
On 3/7/2006 9:18 PM, Duncan Murdoch wrote:
1 day later
#
Duncan Murdoch wrote:
If while running the program you don't need to change either the default 
options of the wrapper or the change which lowlevel function is called 
this approach works well if you calculations are not too complicated.

createWrapper <- function(FUN, Params) {
   as.function(c(replace(formals(FUN), names(Params), Params),
                 body(FUN)))
}

const <- 10

newDefaults <- alist(cat = 2,
                      longname = if(cat == 2){
                        if(!missing(dog)) cat + dog
                        else cat + 2
                      } else cat * const, dog=)

wrapper <- createWrapper(lowlevel, newDefaults)

 > wrapper()
longname =  4
 > wrapper(longname=3)
longname =  3
 > wrapper(long=3)
longname =  3
 > wrapper(3)
longname =  3
 > wrapper(cat=4)
longname =  40
 > wrapper(dog=6)
longname =  8
 > wrapper(cat=4, dog=6)
longname =  40
 > wrapper(long=3, cat=4, dog=6)
longname =  3

Charles