TL;DR
Consider using the "trick" implemented in plot.default
and elsewhere. It is tried and tested.
foo <- function(x, y, ...) {
localSum <- function(..., value) sum(...)
localGrep <- function(..., na.rm) grep(...)
list(sum = localSum(x, ...), grep = localGrep("abc", y, ...))
}
X <- c(1:5, NA, 6:10)
Y <- "xyzabcxyz"
foo(X, Y, na.rm = TRUE, value = TRUE)
## $sum
## [1] 55
##
## $grep
## [1] "xyzabcxyz"
##
I am a bit shocked that the "trick" used by plot.default
hasn't come up here yet. (Maybe it is mentioned elsewhere?)
Recall that plot.default
must process the optional arguments of the internal plot.xy
(for points and lines) as well as those of plot.window
, box
, axis
, and title
. These are all handled through one ...
argument.
Importantly, when we pass (say) lwd = 6
to plot.default
to widen lines in the plot region, plot.default
must ensure that this argument is not seen by box
and axis
, which use lwd
to set the width of the plot region border and axis lines. Hence:
> plot.default(0:1, 0:1, type = "l", lwd = 6)

This feature of plot.default
is implemented using "local" versions of the subfunctions plot.window
, box
, axis
, and title
. The local versions are just wrappers designed to filter out arguments in ...
that the subfunctions cannot be allowed to see (in this case col
, bg
, pch
, cex
, lty
, and lwd
).
> plot.default
function (x, y = NULL, type = "p", xlim = NULL, ylim = NULL,
log = "", main = NULL, sub = NULL, xlab = NULL, ylab = NULL,
ann = par("ann"), axes = TRUE, frame.plot = axes, panel.first = NULL,
panel.last = NULL, asp = NA, xgap.axis = NA, ygap.axis = NA,
...)
{
localAxis <- function(..., col, bg, pch, cex, lty, lwd) Axis(...)
localBox <- function(..., col, bg, pch, cex, lty, lwd) box(...)
localWindow <- function(..., col, bg, pch, cex, lty, lwd) plot.window(...)
localTitle <- function(..., col, bg, pch, cex, lty, lwd) title(...)
xlabel <- if (!missing(x))
deparse1(substitute(x))
ylabel <- if (!missing(y))
deparse1(substitute(y))
xy <- xy.coords(x, y, xlabel, ylabel, log)
xlab <- if (is.null(xlab))
xy$xlab
else xlab
ylab <- if (is.null(ylab))
xy$ylab
else ylab
xlim <- if (is.null(xlim))
range(xy$x[is.finite(xy$x)])
else xlim
ylim <- if (is.null(ylim))
range(xy$y[is.finite(xy$y)])
else ylim
dev.hold()
on.exit(dev.flush())
plot.new()
localWindow(xlim, ylim, log, asp, ...)
panel.first
plot.xy(xy, type, ...)
panel.last
if (axes) {
localAxis(if (is.null(y))
xy$x
else x, side = 1, gap.axis = xgap.axis, ...)
localAxis(if (is.null(y))
x
else y, side = 2, gap.axis = ygap.axis, ...)
}
if (frame.plot)
localBox(...)
if (ann)
localTitle(main = main, sub = sub, xlab = xlab, ylab = ylab,
...)
invisible()
}
<bytecode: 0x10b813010>
<environment: namespace:graphics>
Let's take localTitle
as an example:
localTitle <- function(..., col, bg, pch, cex, lty, lwd) title(...)
plot.default
passes all of the arguments in ...
to localTitle
when it calls
localTitle(main = main, sub = sub, xlab = xlab, ylab = ylab, ...)
but title
only ever sees those arguments not named col
, bg
, pch
, cex
, lty
, or lwd
, since those are included as formal arguments of localTitle
.
Notably, this approach to processing ...
has minimal overhead as it takes full advantage of lazy evaluation. None of the expressions in ...
are evaluated until they are used by the subfunctions.