A classic and very real design problem in R wrapper APIs, is a tension between API convenience (...) and argument safety / correctness.
TL;DR
There is no safe way to blindly forward the same ... to multiple functions.
You must partition, capture, or validate arguments before dispatching them.
The most robust solutions are:
- Explicitly split
... using formals()
- Use named argument lists (
plot_args, save_args)
- Use
rlang::list2() + check_dots_used()
- Capture
... once and validate before forwarding
- Expose helper wrappers instead of raw
...
Anything else will leak arguments into the wrong function.
Why your dummy example is unsafe
wrapper <- function(arg1, arg2, ...) {
plotter(arg1, ...)
qqSave(arg2, ...)
}
This fails silently in multiple ways:
| Problem |
Why |
| Argument name collision |
Same argument name may exist in both |
| Partial matching |
R may partially match arguments |
| Silent drops |
Unused args are silently ignored |
| Wrong dispatch |
w, h, ext meant for save reach ggplot |
| Debugging hell |
No error, wrong behavior |
This is not hypothetical — your qHistogram() already has this risk.
Solution patterns (ranked)
1️⃣ Explicit argument partitioning via formals() (Best general solution)
Capture ... once, then split based on target function signatures.
Pattern
split_dots <- function(dots, fun) {
fnames <- names(formals(fun))
dots[names(dots) %in% fnames]
}
Usage
dots <- list(...)
plot_dots <- split_dots(dots, ggpubr::gghistogram)
save_dots <- split_dots(dots, qqSave)
ggpubr::gghistogram(..., plot_dots)
qqSave(..., save_dots)
Pros
✅ Safe
✅ Scales well
✅ Self-updating if upstream function changes
Cons
❌ Slight overhead
❌ Requires discipline
➡ This is what I’d recommend for ggExpress
2️⃣ Named argument bundles (plot_args, save_args)
Instead of ..., force structure.
qHistogram <- function(
vec,
plot_args = list(),
save_args = list()
) {
do.call(ggpubr::gghistogram, plot_args)
do.call(qqSave, save_args)
}
Pros
✅ Zero ambiguity
✅ Explicit API
✅ Easy to document
Cons
❌ Less ergonomic
❌ More verbose for users
➡ Best for internal or power-user APIs
3️⃣ rlang::list2() + validation (Modern tidyverse style)
dots <- rlang::list2(...)
Then validate:
or explicitly error on unused:
unused <- setdiff(names(dots), allowed_names)
if (length(unused)) abort(paste("Unused args:", unused))
Pros
✅ Early failure
✅ Clear error messages
✅ Tidyverse-consistent
Cons
❌ Still need argument splitting
➡ Great in combination with #1
4️⃣ Argument prefixes (e.g. plot_*, save_*)
qHistogram(..., plot_bins = 30, save_w = 5)
Then strip prefixes:
plot_dots <- dots[startsWith(names(dots), "plot_")]
names(plot_dots) <- sub("^plot_", "", names(plot_dots))
Pros
✅ Very explicit
✅ User-friendly
Cons
❌ Verbose
❌ Less idiomatic R
➡ Works well for large APIs
5️⃣ Declare intentional passthroughs only
qHistogram <- function(..., bins = 30, alpha = 0.5)
Forward only what you explicitly support.
Pros
✅ Maximum safety
✅ Clean docs
Cons
❌ Less flexible
❌ Requires maintenance
➡ Best for stable, curated APIs
Applying this to your qHistogram()
Your current call:
ggpubr::gghistogram(
...,
palette = pal2, ...
)
Problem
Any of these do not belong to gghistogram():
w, h, ext, also.pdf, save.obj
They will silently leak.
Recommended refactor (minimal disruption)
dots <- list(...)
plot_dots <- split_dots(dots, ggpubr::gghistogram)
p <- ggpubr::gghistogram(
data = df,
x = "value",
palette = pal2,
!!!plot_dots
)
Then do not forward dots to qqSave() — you already pass its args explicitly (which is good design).
What you should know (important principles)
1️⃣ ... is not polymorphism
It’s a raw argument bag, not a router.
2️⃣ Silent failure is worse than verbosity
R will happily ignore unknown arguments.
Your users will never know.
3️⃣ Wrapper functions are APIs
Once public, changing argument behavior is breaking.
Design conservatively.
4️⃣ Tidyverse got this right
Look at how dplyr, ggplot2, rlang:
- Capture once
- Validate early
- Dispatch intentionally
My professional recommendation for ggExpress
For exported functions:
✔ Use explicit args + validated ...
✔ Split ... using formals()
✔ Error on unused arguments
For internal helpers:
✔ Use structured argument lists
A classic and very real design problem in R wrapper APIs, is a tension between API convenience (
...) and argument safety / correctness.TL;DR
There is no safe way to blindly forward the same
...to multiple functions.You must partition, capture, or validate arguments before dispatching them.
The most robust solutions are:
...using formals()plot_args,save_args)rlang::list2()+check_dots_used()...once and validate before forwarding...Anything else will leak arguments into the wrong function.
Why your dummy example is unsafe
This fails silently in multiple ways:
w,h,extmeant for save reach ggplotThis is not hypothetical — your
qHistogram()already has this risk.Solution patterns (ranked)
1️⃣ Explicit argument partitioning via
formals()(Best general solution)Capture
...once, then split based on target function signatures.Pattern
Usage
Pros
✅ Safe
✅ Scales well
✅ Self-updating if upstream function changes
Cons
❌ Slight overhead
❌ Requires discipline
➡ This is what I’d recommend for
ggExpress2️⃣ Named argument bundles (
plot_args,save_args)Instead of
..., force structure.Pros
✅ Zero ambiguity
✅ Explicit API
✅ Easy to document
Cons
❌ Less ergonomic
❌ More verbose for users
➡ Best for internal or power-user APIs
3️⃣
rlang::list2()+ validation (Modern tidyverse style)Then validate:
or explicitly error on unused:
Pros
✅ Early failure
✅ Clear error messages
✅ Tidyverse-consistent
Cons
❌ Still need argument splitting
➡ Great in combination with #1
4️⃣ Argument prefixes (e.g.
plot_*,save_*)Then strip prefixes:
Pros
✅ Very explicit
✅ User-friendly
Cons
❌ Verbose
❌ Less idiomatic R
➡ Works well for large APIs
5️⃣ Declare intentional passthroughs only
Forward only what you explicitly support.
Pros
✅ Maximum safety
✅ Clean docs
Cons
❌ Less flexible
❌ Requires maintenance
➡ Best for stable, curated APIs
Applying this to your
qHistogram()Your current call:
Problem
Any of these do not belong to
gghistogram():They will silently leak.
Recommended refactor (minimal disruption)
Then do not forward dots to
qqSave()— you already pass its args explicitly (which is good design).What you should know (important principles)
1️⃣
...is not polymorphismIt’s a raw argument bag, not a router.
2️⃣ Silent failure is worse than verbosity
R will happily ignore unknown arguments.
Your users will never know.
3️⃣ Wrapper functions are APIs
Once public, changing argument behavior is breaking.
Design conservatively.
4️⃣ Tidyverse got this right
Look at how
dplyr,ggplot2,rlang:My professional recommendation for ggExpress
For exported functions:
✔ Use explicit args + validated
...✔ Split
...usingformals()✔ Error on unused arguments
For internal helpers:
✔ Use structured argument lists