Dispatch ellipsis (...) to more than one function [duplicate] - r

I want to write a function that calls both plot() and legend() and it would be ideal if the user could specify a number of additional arguments that are then passed through to either plot() or legend(). I know I can achieve this for one of the two functions using ...:
foo.plot <- function(x,y,...) {
plot(x,y,...)
legend("bottomleft", "bar", pch=1)
}
foo.plot(1,1, xaxt = "n")
This passes xaxt = "n" to plot. But is there a way for example to pass e.g. title = "legend" to the legend() call without prespecifying the arguments in the function header?
Update from the accepted answer: I thought that VitoshKa's way was the most elegant to accomplish what I wanted. However, there were some minor issues that I had to get around with until it worked as I wanted.
At first, I checked which of the parameters I want to pass to legend and which to plot. First step to this end was to see which arguments of legend are unique to legend and not part of plot and/or par:
legend.args <- names(formals(legend))
plot.args <- c(names(formals(plot.default)), names(par()))
dput(legend.args[!(legend.args %in% plot.args)])
I use dput() here, because the line plot.args <- c(names(formals(plot.default)), names(par())) always calls a new empty plot which I did not want. So, I used the output of dput in the following function.
Next, I had to deal with the overlapping arguments (get them via dput(largs.all[(largs.all %in% pargs.all)])). For some this was trivial (e.g., x, y) others get passed to both functions (e.g., pch). But, in my real application I even use other strategies (e.g., different variable names for adj, but not implemented in this example).
Finally, the do.call function had to be changed in two ways. First, the what part (i.e., called functions) needs to be a character (i.e., 'plot' instead of plot). And the argument list must be constructed slightly different.
foo.plot <- function(x,y,...) {
leg.args.unique <- c("legend", "fill", "border", "angle", "density", "box.lwd", "box.lty", "box.col", "pt.bg", "pt.cex", "pt.lwd", "xjust", "yjust", "x.intersp", "y.intersp", "text.width", "text.col", "merge", "trace", "plot", "ncol", "horiz", "title", "inset", "title.col", "title.adj")
leg.args.all <- c(leg.args.unique, "col", "lty", "lwd", "pch", "bty", "bg", "cex", "adj", "xpd")
dots <- list(...)
do.call('plot', c(list(x = x, y = x), dots[!(names(dots) %in% leg.args.unique)]))
do.call('legend', c(list("bottomleft", "bar"), dots[names(dots) %in% leg.args.all]))
}
foo.plot(1,1,pch = 4, title = "legendary", ylim = c(0, 5))
In this example, pch is passed to both plot and legend, title is only passed to legend, and ylim only to plot.
Update 2 based on a comment by Gavin Simpson (see also the comments at Vitoshka's answer):
(i) That's correct.
(ii) It can always be a character. But if you have a variable with the same name as the function, then you need to quote the function name in do.call:
min.plot <- function(x,y,plot=TRUE) if(plot == TRUE) do.call(plot, list(x = x, y = y))
min.plot(1,1)
Error in do.call(plot, list(x = x, y = y)) :
'what' must be a character string or a function
(iii) You can use c(x = 1, y = 1, list()) and it works fine. However, what I really did (not in the example I gave but in my real function) is the following: c(x = 1, y = 1, xlim = c(0, 2), list(bla='foo'))
Please compare this with: c(list(x = 1, y = 1, xlim = c(0, 2)), list(bla='foo'))
In the first case, the list contains two elements xlim, xlim1 and xlim2 (each a scalar), in the latter case the list has only xlim (which is vector of length 2, which is what I wanted).
So, you are right in all your points for my example. But, for my real function (with a lot more variables), I encountered these problems and wanted to document them here. Sorry for being imprecise.

An automatic way:
foo.plot <- function(x,y,...) {
lnames <- names(formals(legend))
pnames <- c(names(formals(plot.default)), names(par()))
dots <- list(...)
do.call('plot', c(list(x = x, y = x), dots[names(dots) %in% pnames]))
do.call('legend', c("bottomleft", "bar", pch = 1, dots[names(dots) %in% lnames]))
}
pch must be filtered from the lnames to avoid duplication in the legend call in case the user supplies 'pch', but you got the idea.
Edited Jan 2012 by Carl W: "do.call" only works with the functions in quotes, as in the updates by Henrik. I edited it here to avoid future confusion.

These things get tricky, and there aren't easy solutions without specifying extra arguments in your function. If you had ... in both the plot and legend calls you'd end up getting warnings when passing in legend-specific arguments. For example, with:
foo.plot <- function(x,y,...) {
plot(x,y,...)
legend("bottomleft", "bar", pch = 1, ...)
}
You get the following warnings:
> foo.plot(1, 1, xjust = 0.5)
Warning messages:
1: In plot.window(...) : "xjust" is not a graphical parameter
2: In plot.xy(xy, type, ...) : "xjust" is not a graphical parameter
3: In axis(side = side, at = at, labels = labels, ...) :
"xjust" is not a graphical parameter
4: In axis(side = side, at = at, labels = labels, ...) :
"xjust" is not a graphical parameter
5: In box(...) : "xjust" is not a graphical parameter
6: In title(...) : "xjust" is not a graphical parameter
There are ways round this problem, see plot.default and its local functions defined as wrappers around functions like axis, box etc. where you'd have something like a localPlot() wrapper, inline function and call that rather than plot() directly.
bar.plot <- function(x, y, pch = 1, ...) {
localPlot <- function(..., legend, fill, border, angle, density,
xjust, yjust, x.intersp, y.intersp,
text.width, text.col, merge, trace, plot = TRUE, ncol,
horiz, title, inset, title.col, box.lwd,
box.lty, box.col, pt.bg, pt.cex, pt.lwd) plot(...)
localPlot(x, y, pch = pch, ...)
legend(x = "bottomleft", legend = "bar", pch = pch, ...)
}
(Quite why the 'plot' argument needs a default is beyond me, but it won't work without giving it the default TRUE.)
Now this works without warnings:
bar.plot(1, 1, xjust = 0.5, title = "foobar", pch = 3)
How you handle graphical parameters like bty for example will be up to you - bty will affect the plot box type and the legend box type. Note also that I've handled 'pch' differently because if someone use that argument in the bar.plot() call, you would be i) using different characters in the legend/plot and you'd get a warning or error about 'pch' matching twice.
As you can see, this starts getting quite tricky...
Joris' Answer provides an interesting solution, which I commented reminded me of control lists arguments in functions like lme(). Here is my version of Joris' Answer implementing the idea this control-list idea:
la.args <- function(x = "bottomleft", legend = "bar", pch = 1, ...)
c(list(x = x, legend = legend, pch = pch), list(...))
foo.plot <- function(x,y, legend.args = la.args(), ...) {
plot(x, y, ...)
do.call(legend, legend.args)
}
Which works like this, using Jori's second example call, suitably modified:
foo.plot(1,1, xaxt = "n", legend.args=la.args(bg = "yellow", title = "legend"))
You can be as complete as you like when setting up the la.args() function - here I only set defaults for the arguments Joris set up, and concatenate any others. It would be easier if la.args() contained all the legend arguments with defaults.

One way around is using lists of arguments in combination with do.call. It's not the most beautiful solution, but it does work.
foo.plot <- function(x,y,legend.args,...) {
la <- list(
x="bottomleft",
legend="bar",
pch=1
)
if (!missing(legend.args)) la <- c(la,legend.args)
plot(x,y,...)
do.call(legend,la)
}
foo.plot(1,1, xaxt = "n")
foo.plot(1,1, xaxt = "n",legend.args=list(bg="yellow",title="legend"))
One drawback is that you cannot specify eg pch=2 for example in the legend.args list. You can get around that with some if clauses, I'll leave it to you to further fiddle around with it.
Edit : see the answer of Gavin Simpson for a better version of this idea.

We could build a formalize function that will make any function compatible with dots :
formalize <- function(f){
# add ... to formals
formals(f) <- c(formals(f), alist(...=))
# release the ... in the local environment
body(f) <- substitute({x;y},list(x = quote(list2env(list(...))),y = body(f)))
f
}
foo.plot <- function(x,y,...) {
legend <- formalize(legend) # definition is changed locally
plot(x,y,...)
legend("bottomleft", "bar", pch = 1, ...)
}
foo.plot(1,1, xaxt = "n", title = "legend")
It shows warnings though, because plot is receiving arguments it doesn't expect.
We could use suppressWarnings on the plot call (but it will suppress all warnings obviously), or we could design another function to make the plot function (or any function) locally more tolerant to dots :
casualize <- function(f){
f_name <- as.character(substitute(f))
f_ns <- getNamespaceName(environment(f))
body(f) <- substitute({
# extract all args
args <- as.list(match.call()[-1]);
# relevant args only
args <- args[intersect(names(formals()),names(args))]
# call initial fun with relevant args
do.call(getExportedValue(f_ns, f_name),args)})
f
}
foo.plot <- function(x,y,...) {
legend <- formalize(legend)
plot <- casualize(plot)
plot(x,y,...)
legend("bottomleft", "bar", pch = 1, ...)
}
foo.plot(1,1, xaxt = "n", title = "legend")
Now it works fine!

A similar solution to the accepted answer, but this one uses list2() and the splice operator !!! (plus its auxiliary function inject()) from the rlang package. It has the advantage of not needing to concatenate named or hardcoded arguments to the "dots" list.
foo.plot <- function(x, y, leg.pos='bottomleft', legend='bar', ...) {
#.Pars.readonly <- c("cin", "cra", "csi", "cxy", "din", "page") # from par()
eval(body(par)[[2]]) # .Pars.readonly <- c(...)
this_params <- names(formals())
par_params <- setdiff(graphics:::.Pars, .Pars.readonly)
plot_params <- setdiff(union(formalArgs(plot.default), par_params), this_params)
legend_params <- setdiff(formalArgs(graphics::legend), this_params)
dots <- rlang::list2(...)
plot_args <- dots[names(dots) %in% plot_params]
legend_args <- dots[names(dots) %in% legend_params]
rlang::inject(plot.default(x, y, !!!plot_args))
rlang::inject(graphics::legend(leg.pos, legend=legend, !!!legend_args))
}

Just wanted to add a general solution that I've implemented. I wrote a function dots_parser which takes the function you want to call, any named arguments, and the ellipsis, and then runs the function with only the appropriate arguments.
dots_parser <- function(FUN, ...) {
argnames <- names(formals(FUN))
dots <- list(...)
return(do.call(FUN, dots[names(dots) %in% argnames]))
}
So for instance, if code previously would've looked like this:
func1(a = myvaluea, b = myvalueb, ...)
But that returns an error because ... includes arguments not accepted by func1, this now works:
dots_parser(func1, a = myvaluea, b = myvalueb, ...)

Related

How to overwrite a hardcoded function argument with the dots (...) parameter?

I am trying to make a plot function with a set of default values and the flexibility to change these values by using any argument the plot function accepts inside the dots (...) argument. An example:
PlotIt <- function(x, y, ...) {
plot(x, y, type = "l", asp = 1, ...)
}
x <- 1:10
y <- 10:1
PlotIt(x = x, y = y)
# Returns a plot
PlotIt(x = x, y = y, asp = NA)
# Error in plot.default(x, y, type = "l", asp = 1, ...) :
# formal argument "asp" matched by multiple actual arguments
The error is naturally because I try to pass the asp argument twice into plot. So far my best clumsy attempt is to make an if-else statement to take this into account (the approach is modified from here):
PlotIt2 <- function(x, y, ...) {
mc <- match.call(expand.dots = FALSE)
if(names(mc$...) %in% "asp") {
plot(x, y, type = "l", ...)
} else {
plot(x, y, type = "l", asp = 1, ...)
}
}
PlotIt2(x = x, y = y, asp = NA)
# works
To do this for all possible parameters one can set in with the ... argument, I would need to write a long if-else statement. Is there a more elegant way of doing this?
The question is related to this one with the difference that I want to automatically overwrite all parameters set by the ... argument.
If you want to use base R only,
you can put everything in a list and remove duplicates based on the argument names
(making sure that the defaults are last so that they are removed if they are present in ...):
PlotIt <- function(x, y, ...) {
arguments <- list(
x = x,
y = y,
...,
type = "l",
asp = 1
)
arguments <- arguments[!duplicated(names(arguments))]
do.call("plot", arguments)
}
If you don't mind depending on rlang,
you could also do the following,
using .homonyms to get the same functionality
(and check the plot's labels for the axes,
it'll be different between the base R and rlang versions):
PlotIt <- function(x, y, ...) {
require("rlang")
arguments <- rlang::dots_list(
rlang::expr(x),
rlang::expr(y),
...,
type = "l",
asp = 1,
.homonyms = "first"
)
call <- rlang::call2("plot", !!!arguments)
eval(call)
}

R: How to use ellipsis (...) in function argument if the function contains multiple inner ellipsed functions? [duplicate]

I want to write a function that calls both plot() and legend() and it would be ideal if the user could specify a number of additional arguments that are then passed through to either plot() or legend(). I know I can achieve this for one of the two functions using ...:
foo.plot <- function(x,y,...) {
plot(x,y,...)
legend("bottomleft", "bar", pch=1)
}
foo.plot(1,1, xaxt = "n")
This passes xaxt = "n" to plot. But is there a way for example to pass e.g. title = "legend" to the legend() call without prespecifying the arguments in the function header?
Update from the accepted answer: I thought that VitoshKa's way was the most elegant to accomplish what I wanted. However, there were some minor issues that I had to get around with until it worked as I wanted.
At first, I checked which of the parameters I want to pass to legend and which to plot. First step to this end was to see which arguments of legend are unique to legend and not part of plot and/or par:
legend.args <- names(formals(legend))
plot.args <- c(names(formals(plot.default)), names(par()))
dput(legend.args[!(legend.args %in% plot.args)])
I use dput() here, because the line plot.args <- c(names(formals(plot.default)), names(par())) always calls a new empty plot which I did not want. So, I used the output of dput in the following function.
Next, I had to deal with the overlapping arguments (get them via dput(largs.all[(largs.all %in% pargs.all)])). For some this was trivial (e.g., x, y) others get passed to both functions (e.g., pch). But, in my real application I even use other strategies (e.g., different variable names for adj, but not implemented in this example).
Finally, the do.call function had to be changed in two ways. First, the what part (i.e., called functions) needs to be a character (i.e., 'plot' instead of plot). And the argument list must be constructed slightly different.
foo.plot <- function(x,y,...) {
leg.args.unique <- c("legend", "fill", "border", "angle", "density", "box.lwd", "box.lty", "box.col", "pt.bg", "pt.cex", "pt.lwd", "xjust", "yjust", "x.intersp", "y.intersp", "text.width", "text.col", "merge", "trace", "plot", "ncol", "horiz", "title", "inset", "title.col", "title.adj")
leg.args.all <- c(leg.args.unique, "col", "lty", "lwd", "pch", "bty", "bg", "cex", "adj", "xpd")
dots <- list(...)
do.call('plot', c(list(x = x, y = x), dots[!(names(dots) %in% leg.args.unique)]))
do.call('legend', c(list("bottomleft", "bar"), dots[names(dots) %in% leg.args.all]))
}
foo.plot(1,1,pch = 4, title = "legendary", ylim = c(0, 5))
In this example, pch is passed to both plot and legend, title is only passed to legend, and ylim only to plot.
Update 2 based on a comment by Gavin Simpson (see also the comments at Vitoshka's answer):
(i) That's correct.
(ii) It can always be a character. But if you have a variable with the same name as the function, then you need to quote the function name in do.call:
min.plot <- function(x,y,plot=TRUE) if(plot == TRUE) do.call(plot, list(x = x, y = y))
min.plot(1,1)
Error in do.call(plot, list(x = x, y = y)) :
'what' must be a character string or a function
(iii) You can use c(x = 1, y = 1, list()) and it works fine. However, what I really did (not in the example I gave but in my real function) is the following: c(x = 1, y = 1, xlim = c(0, 2), list(bla='foo'))
Please compare this with: c(list(x = 1, y = 1, xlim = c(0, 2)), list(bla='foo'))
In the first case, the list contains two elements xlim, xlim1 and xlim2 (each a scalar), in the latter case the list has only xlim (which is vector of length 2, which is what I wanted).
So, you are right in all your points for my example. But, for my real function (with a lot more variables), I encountered these problems and wanted to document them here. Sorry for being imprecise.
An automatic way:
foo.plot <- function(x,y,...) {
lnames <- names(formals(legend))
pnames <- c(names(formals(plot.default)), names(par()))
dots <- list(...)
do.call('plot', c(list(x = x, y = x), dots[names(dots) %in% pnames]))
do.call('legend', c("bottomleft", "bar", pch = 1, dots[names(dots) %in% lnames]))
}
pch must be filtered from the lnames to avoid duplication in the legend call in case the user supplies 'pch', but you got the idea.
Edited Jan 2012 by Carl W: "do.call" only works with the functions in quotes, as in the updates by Henrik. I edited it here to avoid future confusion.
These things get tricky, and there aren't easy solutions without specifying extra arguments in your function. If you had ... in both the plot and legend calls you'd end up getting warnings when passing in legend-specific arguments. For example, with:
foo.plot <- function(x,y,...) {
plot(x,y,...)
legend("bottomleft", "bar", pch = 1, ...)
}
You get the following warnings:
> foo.plot(1, 1, xjust = 0.5)
Warning messages:
1: In plot.window(...) : "xjust" is not a graphical parameter
2: In plot.xy(xy, type, ...) : "xjust" is not a graphical parameter
3: In axis(side = side, at = at, labels = labels, ...) :
"xjust" is not a graphical parameter
4: In axis(side = side, at = at, labels = labels, ...) :
"xjust" is not a graphical parameter
5: In box(...) : "xjust" is not a graphical parameter
6: In title(...) : "xjust" is not a graphical parameter
There are ways round this problem, see plot.default and its local functions defined as wrappers around functions like axis, box etc. where you'd have something like a localPlot() wrapper, inline function and call that rather than plot() directly.
bar.plot <- function(x, y, pch = 1, ...) {
localPlot <- function(..., legend, fill, border, angle, density,
xjust, yjust, x.intersp, y.intersp,
text.width, text.col, merge, trace, plot = TRUE, ncol,
horiz, title, inset, title.col, box.lwd,
box.lty, box.col, pt.bg, pt.cex, pt.lwd) plot(...)
localPlot(x, y, pch = pch, ...)
legend(x = "bottomleft", legend = "bar", pch = pch, ...)
}
(Quite why the 'plot' argument needs a default is beyond me, but it won't work without giving it the default TRUE.)
Now this works without warnings:
bar.plot(1, 1, xjust = 0.5, title = "foobar", pch = 3)
How you handle graphical parameters like bty for example will be up to you - bty will affect the plot box type and the legend box type. Note also that I've handled 'pch' differently because if someone use that argument in the bar.plot() call, you would be i) using different characters in the legend/plot and you'd get a warning or error about 'pch' matching twice.
As you can see, this starts getting quite tricky...
Joris' Answer provides an interesting solution, which I commented reminded me of control lists arguments in functions like lme(). Here is my version of Joris' Answer implementing the idea this control-list idea:
la.args <- function(x = "bottomleft", legend = "bar", pch = 1, ...)
c(list(x = x, legend = legend, pch = pch), list(...))
foo.plot <- function(x,y, legend.args = la.args(), ...) {
plot(x, y, ...)
do.call(legend, legend.args)
}
Which works like this, using Jori's second example call, suitably modified:
foo.plot(1,1, xaxt = "n", legend.args=la.args(bg = "yellow", title = "legend"))
You can be as complete as you like when setting up the la.args() function - here I only set defaults for the arguments Joris set up, and concatenate any others. It would be easier if la.args() contained all the legend arguments with defaults.
One way around is using lists of arguments in combination with do.call. It's not the most beautiful solution, but it does work.
foo.plot <- function(x,y,legend.args,...) {
la <- list(
x="bottomleft",
legend="bar",
pch=1
)
if (!missing(legend.args)) la <- c(la,legend.args)
plot(x,y,...)
do.call(legend,la)
}
foo.plot(1,1, xaxt = "n")
foo.plot(1,1, xaxt = "n",legend.args=list(bg="yellow",title="legend"))
One drawback is that you cannot specify eg pch=2 for example in the legend.args list. You can get around that with some if clauses, I'll leave it to you to further fiddle around with it.
Edit : see the answer of Gavin Simpson for a better version of this idea.
We could build a formalize function that will make any function compatible with dots :
formalize <- function(f){
# add ... to formals
formals(f) <- c(formals(f), alist(...=))
# release the ... in the local environment
body(f) <- substitute({x;y},list(x = quote(list2env(list(...))),y = body(f)))
f
}
foo.plot <- function(x,y,...) {
legend <- formalize(legend) # definition is changed locally
plot(x,y,...)
legend("bottomleft", "bar", pch = 1, ...)
}
foo.plot(1,1, xaxt = "n", title = "legend")
It shows warnings though, because plot is receiving arguments it doesn't expect.
We could use suppressWarnings on the plot call (but it will suppress all warnings obviously), or we could design another function to make the plot function (or any function) locally more tolerant to dots :
casualize <- function(f){
f_name <- as.character(substitute(f))
f_ns <- getNamespaceName(environment(f))
body(f) <- substitute({
# extract all args
args <- as.list(match.call()[-1]);
# relevant args only
args <- args[intersect(names(formals()),names(args))]
# call initial fun with relevant args
do.call(getExportedValue(f_ns, f_name),args)})
f
}
foo.plot <- function(x,y,...) {
legend <- formalize(legend)
plot <- casualize(plot)
plot(x,y,...)
legend("bottomleft", "bar", pch = 1, ...)
}
foo.plot(1,1, xaxt = "n", title = "legend")
Now it works fine!
A similar solution to the accepted answer, but this one uses list2() and the splice operator !!! (plus its auxiliary function inject()) from the rlang package. It has the advantage of not needing to concatenate named or hardcoded arguments to the "dots" list.
foo.plot <- function(x, y, leg.pos='bottomleft', legend='bar', ...) {
#.Pars.readonly <- c("cin", "cra", "csi", "cxy", "din", "page") # from par()
eval(body(par)[[2]]) # .Pars.readonly <- c(...)
this_params <- names(formals())
par_params <- setdiff(graphics:::.Pars, .Pars.readonly)
plot_params <- setdiff(union(formalArgs(plot.default), par_params), this_params)
legend_params <- setdiff(formalArgs(graphics::legend), this_params)
dots <- rlang::list2(...)
plot_args <- dots[names(dots) %in% plot_params]
legend_args <- dots[names(dots) %in% legend_params]
rlang::inject(plot.default(x, y, !!!plot_args))
rlang::inject(graphics::legend(leg.pos, legend=legend, !!!legend_args))
}
Just wanted to add a general solution that I've implemented. I wrote a function dots_parser which takes the function you want to call, any named arguments, and the ellipsis, and then runs the function with only the appropriate arguments.
dots_parser <- function(FUN, ...) {
argnames <- names(formals(FUN))
dots <- list(...)
return(do.call(FUN, dots[names(dots) %in% argnames]))
}
So for instance, if code previously would've looked like this:
func1(a = myvaluea, b = myvalueb, ...)
But that returns an error because ... includes arguments not accepted by func1, this now works:
dots_parser(func1, a = myvaluea, b = myvalueb, ...)

How to let R recognize a vector of arguments in the ellipsis?

I'm trying to work smartly with the ellipsis (...) argument in R and have some problems.
I am trying to pass some default arguments at the beginning of the function without cluttering up the argument area of the function by using ... and overriding if they are provided there. But somehow the ellipsis argument doesn't seem to pick up my full vector
test <- function(dat,
# I don't want to have to put default col,
# ylim, ylab, lty arguments etc. here
...) {
# but here, to be overruled if hasArg finds it
color <- "red"
if(hasArg(col)) { # tried it with both "col" and col
message(paste("I have col:", col))
color <- col
}
plot(dat, col = color)
}
Function call:
test(data.frame(x = 1:10, y = 11:20), col = c("purple", "green", "blue"))
Throws the error:
Error in paste("I have col:", col) (from #8) :
cannot coerce type 'closure' to vector of type 'character'
So something is going wrong here. If I pass the ellipsis arguments to the plot function immediately it does work without error.
You need to do this, by collecting/packing ... into a list, if you want to use its contents inside the function.
test <- function(dat,
# I don't want to have to put default col,
# ylim, ylab, lty arguments etc. here
...) {
opt <- list(...)
color <- "red"
if(!is.null(opt$col)) { # tried it with both "col" and col
message(paste("I have col:", opt$col))
color <- opt$col
}
plot(dat, col = color)
}
test(data.frame(x = 1:10, y = 11:20), col = c("purple", "green", "blue"))
The problem in your original code, is that args() or hasArg() only works for formal arguments in function call. So when you pass in col = c("purple", "green", "blue"), hasArg() knows there is a formal argument col, but does not evaluate it. Therefore, inside the function, there is no actual col variable to be found (you can use a debugger to verify this). Interestingly, there is a function col() from R base package, so this function is passed to paste. As a result, you get an error message when trying to concatenate a character string and a "closure".

Pass arguments in nested function to update default arguments

I have nested functions and wish to pass arguments to the deepest function. That deepest function will already have default arguments, so I will be updating those argument values.
My mwe is using plot(), but in reality I'm working with png(), with default height and width arguments.
Any suggestions?
f1 <- function(...){ f2(...)}
f2 <- function(...){ f3(...)}
f3 <- function(...){ plot(xlab="hello1", ...)}
#this works
f1(x=1:10,y=rnorm(10),type='b')
# I want to update the default xlab value, but it fails:
f1(x=1:10,y=rnorm(10),type='b', xlab='hello2')
In your f3(), "hello1" is not a default value for xlab in the list of function's formal arguments. It is instead the supplied value in the function body, so there's no way to override it:
f3 <- function(...){ plot(xlab="hello1", ...)}
I suspect you meant instead to do something like this.
f1 <- function(...){ f2(...)}
f2 <- function(...){ f3(...)}
f3 <- function(..., xlab="hello1") plot(..., xlab=xlab)
## Then check that it works
par(mfcol=c(1,2))
f1(x=1:10,y=rnorm(10),type='b')
f1(x=1:10,y=rnorm(10),type='b', xlab='hello2')
(Do notice that the formal argument xlab must follow the ... argument here, so that it can only be matched exactly (and not by partial matching). Otherwise, in the absence of an argument named xlab, it'll get matched by an argument named x, potentially (and actually here) causing you a lot of grief.)
My usual approach for modifying arguments in ... is as follows:
f1 = function(...) {
dots = list(...)
if (!('ylab' %in% names(dots))) {
dots$ylab = 'hello'
}
do.call(plot, dots)
}
# check results
f1(x = 1:10, y = rnorm(10))
f1(x = 1:10, y = rnorm(10), ylab = 'hi')
What happens here is that ... is captured in a list called dots. Next, R checks if this list dots contains any information about ylab. If there is no information, we set it to a specified value. If there is information, we do nothing. Last, do.call(a, b) is a function that basically stands voor execute function a with arguments b.
edit
This works better with multiple default arguments (and probably also better in general).
f1 = function(...) {
# capture ... in a list
dots = list(...)
# default arguments with their values
def.vals = list(bty = 'n', xlab = 'hello', las = 1)
# find elements in dots by names of def.vals. store those that are NULL
ind = unlist(lapply(dots[names(def.vals)], is.null))
# fill empty elements with default values
dots[names(def.vals)[ind]] = def.vals[ind]
# do plot
do.call(plot, dots)
}
f1(x = 1:10, y = rnorm(10), ylab = 'hi', bty = 'l')

Default argument in R function (formal argument matched by multiple actual arguments)

Simple question, I hope. I want to write a plotting function that has a default value for the y-axis label if the user doesn't specify. I'd also like to allow the ... argument for other plotting parameters, and allow the user to set ylab manually. But I can't figure out how to do this.
# simple scatterplot function with a default ylab
scatter <- function(x,y, ...) {
plot(x, y, ylab="Default y-axis label", ...)
}
# generate data
x <- rnorm(100)
y <- x+rnorm(100)
# use the default
scatter(x,y)
# here I want to use my own label, but I get an error!
scatter(x, y, ylab="New y-axis label")
The error I get is:
Error in plot.default(x, y, ylab = "Default y-axis label", ...) :
formal argument "ylab" matched by multiple actual arguments
I understand the problem, but I don't know the best way to fix it. Thanks for the help!
EDIT: I realize I can do something like
scatter <- function(x,y,ylab = "Default y-axis label", ...) {
plot(x, y, ylab= ylab, ...)
}
...but if I'm writing a package to submit to CRAN, and I have lots of default options I'd like to fiddle with, I don't want to have to document all these standard plotting arguments because they're used in my function definition.
Try doing this instead:
scatter <- function(x,y,ylab = "Default y-axis label", ...) {
plot(x, y, ylab= ylab, ...)
}
Expanding slightly on Arun's answer, this is a sketch of one route to take if you have many arguments:
def_args <- list(ylab = "Default Label",xlab = "Default Label")
scatter <- function(x,y, ...) {
cl <- as.list(match.call())[-1L]
do.call("plot",c(cl,def_args[!names(def_args) %in% names(cl)]))
}
Some thought would be needed to decide how you want to handle partial matching of arguments (if at all). e.g. perhaps something like this:
scatter <- function(x,y, ...) {
cl <- as.list(match.call())[-1L]
names(cl) <- match.arg(names(cl),
names(formals(plot.default)),several.ok = TRUE)
do.call("plot",c(cl,def_args[!names(def_args) %in% names(cl)]))
}
would handle partial matching of arguments.
One way using match.call to check if ylab has been specified as an argument:
scatter <- function(x,y, ...) {
mcall = as.list(match.call())[-1L]
if (!"ylab" %in% names(mcall))
plot(x, y, ylab="Default y-axis label", ...)
else plot(x, y, ...)
}
As mentioned under comment list(...) is a nicer way to get just the dots argument expanded than having to get all the formal arguments with match.call.
You might also try using pmatch instead of %in% for partial matching of arguments.
I use a function to build an argument list. In my case, I do not care about partially matching argument names, which is good because this won't support it.
# Create a list of input arguments.
# Allow arguments to be specified multiple times, first definition wins.
# The resulting list is intended to be passed to do.call().
make.args <- function(..., PRE.ARGS=list(), POST.ARGS=list()) {
a <- list()
l <- c(PRE.ARGS, list(...), POST.ARGS)
for (name in unique(names(l))) {
a[[name]] <- l[[name]] # First occurrence will be found.
}
return(a)
}
An example of its use:
plot.rate <- function(col, cond=NULL, ...) {
col <- paste(col, collapse=' + ')
f <- paste(col, '~ Rate')
if (!is.null(cond)) {
cond <- paste(cond, collapse=' + ')
f <- paste(f, cond, sep='|')
}
arg.list <- make.args(...
, x = as.formula(f)
, main=col
, grid=TRUE
, scales=list(x=list(alternating=1) # bottom(/left)
, y=list(alternating=3)) # both
, xlab='x RTM'
)
do.call(xyplot, arg.list)
}

Resources