Chapter 8 Apendix A4 – rogramming tidbits

8.1 The Ellipsis

bar = function(x, polynomial = 2){
  return(x^polynomial)
}

foo = function(x,...){
  z = bar(x,...)
  return(z)
}
foo(6)
## [1] 36
foo(6, polynomial = 3)
## [1] 216
foo(6, polynomial = 2)
## [1] 36

8.2 match.call()

The R documentation states “match.call retunrs a call in which all of the specified arguments are specified by their full names.”

Cool. What?

What this does is takes in a function, in our case the lm function and returns an object which is of class "call". This disassembles the specified function from its input arguments, allowing us to manipulate arguments without evaluating them.

Suppose we have a function

foo = function(x,y){
  bar = match.call()
  names = match(c('x', 'y'), names(bar))
  args = list(bar, names)
  return(args)
}

foo(2,4)
## [[1]]
## foo(x = 2, y = 4)
## 
## [[2]]
## [1] 2 3

We see the output of match.call is effective a regurgitation of our input. What match.call has done is disassemble our function without evaluating it so we can index into our arguments directly. match then allows us to explicitly define the arguments of a function as new variables without evaluating our function.

Agan, this all seems relatively etherial and unnecessary. So, let’s look at a practical example of why this is useful. Say we wanted to write our own function that takes in a user specified formula and outputs a design matrix data frame using the built in stats::model.frame function.

foo = function(formula, data, weights){
  model = stats::model.frame(formula = formula, data = data, weights = weights)
}
foo(y~x, data = data.frame(y = rnorm(10), x = runif(10)))
## Error in model.frame.default(formula = formula, data = data, weights = weights): invalid type (closure) for variable '(weights)'

You see we get an error here. The reason being that we didn’t specify a default value for our weights input. As a result, R tries to use a built in function stats::weights to fill in the missing weights argument. For a small function like this it would be trivial to add in weights = NULL as a default argument in the inner function, but for large functions with dozens of arguments, this is tedious and unnecessary.

We could circumvent this by using the match.call argument.

foo = function(formula, data, weights){
  mf = match.call() #freeze the function and regurgitate it as a call class
  m = match(c('formula', 'data', 'weights'), names(mf),0L) #extract arguments that are relevant for our lower level function.
  mf = mf[c(1L, m)] 
  mf$drop.unused.levels = TRUE #gets rid of unused arguments
  mf[[1L]] = quote(stats::model.frame)#replace 'foo' with 'stats::modelframe'
  mf
}


foo(y~x, data = data.frame(y = rnorm(10), x = runif(10)))
## stats::model.frame(formula = y ~ x, data = data.frame(y = rnorm(10), 
##     x = runif(10)), drop.unused.levels = TRUE)

So you see our output is now a respecified form of our orginal function. However, also note it has not yet been evaluated. We can add one more line of code to unfreeze time and evaluate the expression.

foo = function(formula, data, weights){
  mf = match.call() #freeze the function and regurgitate it as a call class
  m = match(c('formula', 'data', 'weights'), names(mf),0L) #extract arguments that are relevant for our lower level function.
  mf = mf[c(1L, m)] 
  mf$drop.unused.levels = TRUE #gets rid of unused arguments
  mf[[1L]] = quote(stats::model.frame)#replace 'foo' with 'stats::modelframe'
  eval(mf)
}


foo(y~x, data = data.frame(y = rnorm(10), x = runif(10)))
##             y         x
## 1   0.7077870 0.5346722
## 2   0.5259239 0.8335249
## 3   0.6047109 0.9842342
## 4   0.2054106 0.8383571
## 5   1.2202408 0.4338031
## 6   0.8241745 0.4732692
## 7  -0.7060031 0.8553911
## 8   0.6882589 0.6423501
## 9   1.0299911 0.4453169
## 10  0.5221993 0.8782426

As you can see, we have effectively redefined our function without evaluating it. Again, this can be completely ciorcumvented by specifying default arguments, but often this is an unreasonable task for large projects.

You can return from your side quest here

P.S Another benefit of this method is it allows you to specify arguments in your function that have the same name as .Primitives. All-in-all its just another defensive programming strategy that can also be leveraged to convert between functions within other functions.