mathematical.coffee
mathematical.coffee

Reputation: 56935

Prevent partial argument matching

I have an R function:

myFunc <- function(x, base='') {
}

I am now extending the function, allowing a set of arbitrary extra arguments:

myFunc <- function(x, base='', ...) {
}

How may I disable partial argument matching on the base parameter? I cannot put the ... before base='' because I'd like to maintain backwards compatibility of the function (it is often called as myFunction('somevalue', 'someothervalue') without base being explicitly named).

I got stung by calling my function like so:

myFunc(x, b='foo')

I want this to mean base='', b='foo', but R uses partial matching and assumes base='foo'.

Is there some code I can insert in myFunc to determine what argument names were passed in and only match the exact "base" to the base parameter, otherwise grouping it in as part of the ...?

Upvotes: 20

Views: 2332

Answers (6)

csgillespie
csgillespie

Reputation: 60492

The easiest way is to simply set some R options. In particular,

  • warnPartialMatchArgs: logical. If true, warns if partial matching is used in argument matching.
  • warnPartialMatchAttr: logical. If true, warns if partial matching is used in extracting attributes via attr.
  • warnPartialMatchDollar: logical. If true, warns if partial matching is used for extraction by $.

Setting these variables will now raise warnings, that you can turn into errors if you wish.

Upvotes: 4

mb706
mb706

Reputation: 672

It is possible to use sys.call() to get access to function arguments as given by the caller. Care must be taken since sys.call() does not evaluate the arguments and instead gives you the expression of the call. This gets especially difficult when the function is called with ... as an argument: The sys.call() will only contain the ..., not their value. It is, however, possible to take the sys.call() and evaluate it as an argument list for another function, for example list(). This evaluates all promises and throws away some information, but I can't see how to get around this when trying to circumvent R's internal matching.

One idea would be to simulate strict matching. I appended a helper function that does exactly this if called as the first command in a function:

fun = function(x, base='', ...) {
  strictify()  # make matching strict

  list(x, base, ...)
}

This filters out nonmatching args:

> fun(10, b = 20)                                                                                                                                                                                   
[[1]]                                                                                                                                                                                               
[1] 10

[[2]]
[1] ""

$b
[1] 20

and should also work in most other cases (with or without ..., with arguments to the right of the ..., with argument default values). The only thing it does not work with is non-standard evaluation, e.g. when trying to get the expression of an argument using substitute(arg).

The Helper Function

strictify <- function() {
  # remove argument values from the function
  # since matching already happened
  parenv <- parent.frame()  # environment of the calling function
  rm(list=ls(parenv), envir=parenv)  # clear that environment

  # get the arguments
  scall <- sys.call(-1)  # 'call' of the calling function
  callingfun <- scall[[1]]
  scall[[1]] <- quote(`list`)
  args <- eval.parent(scall, 2)  # 'args' is now a list with all arguments

  # if none of the argument are named, we need to set the
  # names() of args explicitly
  if (is.null(names(args))) {
    names(args) <- rep("", length(args))
  }

  # get the function header ('formals') of the calling function
  callfun.object <- eval.parent(callingfun, 2)
  callfun.header <- formals(callfun.object)
  # create a dummy function that just gives us a link to its environment.
  # We will use this environment to access the parameter values. We
  # are not using the parameter values directly, since the default
  # parameter evaluation of R is pretty complicated.
  # (Consider fun <- function(x=y, y=x) { x } -- fun(x=3) and
  # fun(y=3) both return 3)
  dummyfun <- call("function", callfun.header, quote(environment()))
  dummyfun <- eval(dummyfun, envir=environment(callfun.object))
  parnames <- names(callfun.header)

  # Sort out the parameters that didn't match anything
  argsplit <- split(args, names(args) %in% c("", parnames))
  matching.args <- c(list(), argsplit$`TRUE`)
  nonmatching.arg.names <- names(argsplit$`FALSE`)

  # collect all arguments that match something (or are just
  # positional) into 'parenv'. If this includes '...', it will
  # be overwritten later.
  source.env <- do.call(dummyfun, matching.args)
  for (varname in ls(source.env, all.names=TRUE)) {
    parenv[[varname]] <- source.env[[varname]]
  }

  if (!"..." %in% parnames) {
    # Check if some parameters did not match. It is possible to get
    # here if an argument only partially matches.
    if (length(nonmatching.arg.names)) {
      stop(sprintf("Nonmatching arguments: %s",
          paste(nonmatching.arg.names, collapse=", ")))
    }
  } else {
    # we manually collect all arguments that fall into '...'. This is
    # not trivial. First we look how many arguments before the '...'
    # were not matched by a named argument:
    open.args <- setdiff(parnames, names(args))
    taken.unnamed.args <- min(which(open.args == "...")) - 1
    # We throw all parameters that are unmatched into the '...', but we
    # remove the first `taken.unnamed.args` from this, since they go on
    # filling the unmatched parameters before the '...'.
    unmatched <- args[!names(args) %in% parnames]
    unmatched[which(names(unmatched) == "")[seq_len(taken.unnamed.args)]] <- NULL
    # we can just copy the '...' from a dummy environment that we create
    # here.
    dotsenv <- do.call(function(...) environment(), unmatched)
    parenv[["..."]] <- dotsenv[["..."]]
  }
}

It would also be possible to have a function that converts a normally matching function into a strictly matching function, e.g.

strict.fun = strictificate(fun)

but that would use the same kinds of tricks.

Upvotes: 0

Bing
Bing

Reputation: 1103

This is a bit late. But for future reference, I got this idea.

The partial matching can be avoided using quoted names. In a function, use sys.call() to the parameters.

    > myFunc <- function(x, base="base", ...) {
+     ## get the arguments
+     ss=sys.call()
+     
+     ## positional arguments can be retrieved using numbers
+     print(paste("ss[[2]]=",ss[[2]]))
+     
+     ## named arguments, no partial matching
+     print(ss[['base']]) ## NULL
+     
+     ## named arguments, no partial matching
+     print(ss[['b']]) ## "a"
+     
+     ## regular call, partially matched
+     print(base) ## "a"
+     
+     ## because 'b' is matched to 'base', 
+     ## 'b' does not exist, cause an error
+     print(b)
+ }
> 
> myFunc(x=1,b='a')
[1] "ss[[2]]= 1"
NULL
[1] "a"
[1] "a"
Error in print(b) : object 'b' not found
> myFunc(1,base="b")
[1] "ss[[2]]= 1"
[1] "b"
NULL
[1] "b"
Error in print(b) : object 'b' not found
> myFunc(2,"c")
[1] "ss[[2]]= 2"
NULL
NULL
[1] "c"
Error in print(b) : object 'b' not found
> 

Upvotes: 0

mathematical.coffee
mathematical.coffee

Reputation: 56935

Just arrived on another way to solve this, prompted by @Hemmo.

Use sys.call() to know how myFunc was called (with no partial argument matching, use match.call for that):

myFunc <- function(x, base='', ...) {
    x <- sys.call() # x[[1]] is myFunc, x[[2]] is x, ...
    argnames <- names(x)
    if (is.null(x$base)) {
        # if x[[3]] has no argname then it is the 'base' argument (positional)
        base <- ifelse(argnames[3] == '', x[[3]], '')
    }
    # (the rest of the `...` is also in x, perhaps I can filter it out by
    #  comparing argnames against names(formals(myFunc)) .

}

Upvotes: 2

Josh O&#39;Brien
Josh O&#39;Brien

Reputation: 162401

Here's an idea:

myFunc <- function(x, .BASE = '', ..., base = .BASE) {
    base
}

## Takes fully matching named arguments       
myFunc(x = "somevalue", base = "someothervalue")
# [1] "someothervalue"

## Positional matching works
myFunc("somevalue", "someothervalue")
# [1] "someothervalue"

## Partial matching _doesn't_ work, as desired
myFunc("somevalue", b="someothervalue")
# [1] ""

Upvotes: 8

Ricardo Saporta
Ricardo Saporta

Reputation: 55390

this is a disgustingly horrible hack, but it might get the job done:

myFunc <- function(x, base='', b=NULL, ba=NULL, bas=NULL, ...) {
  dots <- list(b=b, ba=ba, bas=bas, ...)
  #..
}

Upvotes: -2

Related Questions