Skip to content

Design for Wrapper Functions Calling Multiple Internals & Safely Routing ... Arguments in Wrapper Functions #71

@vertesy

Description

@vertesy

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:

  1. Explicitly split ... using formals()
  2. Use named argument lists (plot_args, save_args)
  3. Use rlang::list2() + check_dots_used()
  4. Capture ... once and validate before forwarding
  5. 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:

rlang::check_dots_used()

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions