Introduction to dots

If you wish, you can view this vignette online here.

## Track time spent on making the vignette
startTime <- Sys.time()

## Bib setup
library('knitcitations')

## Load knitcitations with a clean bibliography
cleanbib()
cite_options(hyperlink = 'to.doc', citation_format = 'text', style = 'html')
# Note links won't show for now due to the following issue
# https://github.com/cboettig/knitcitations/issues/63

## Write bibliography information
write.bibtex(c(knitcitations = citation('knitcitations'),
    dots = citation('dots'), 
    knitrBootstrap = citation('knitrBootstrap'), 
    knitr = citation('knitr')[3],
    rmarkdown = citation('rmarkdown')),
    file = 'dotsRef.bib')
bib <- read.bibtex('dotsRef.bib')

## Assign short names
names(bib) <- c('knitcitations', 'dots', 'knitrBootstrap',
    'knitr', 'rmarkdown')

Overview

dots r citep(bib[['dots']]) is a simple package that helps you simplify your function definitions by using the ... arguments. This reduces the complexity a new user of your package will face when reading its documentation. At the same time, dots allows you to define advanced arguments which you can then define inside your functions. Experienced users will be able to use them thus controlling those fine parameters most users do not need.

dots()

The main function in this package is dots(). It will look inside ... to see if name has been used. If it has, it will return its value. Otherwise, it returns a default value.

## Load the library
library('dots')
## Explore code
## Alias: advanced...()
dots

The idea of this fairly simple function is to use it inside your functions to define advanced arguments and give them default values.

The following example defines myFun() which needs the argument x to work. Inside myFun(), dots() is used to define the advanced argument verbose which the developer of myFun() thinks users will rarely want to use. This developer still wants to give users the flexibility to control this argument but doesn't want to confuse the majority of its users with a huge argument list.

myFun <- function(x, ...) {
    ## In all the cases, the user has to specify 'x' for this function to work

    ## However, only a few users will want to make the function print status
    ## reports (controlled by the 'verbose' argument)

    #' @param verbose Controls whether to print status reports or not.
    #' Use value from global 'verbose' option if available. Otherwise, use 
    #' \code{FALSE}
    verbose <- dots('verbose', getOption('verbose', FALSE), ...)
    if(verbose)
        message(paste(Sys.time(), 'myFun: performing analysis'))

    ## In this case, it's just a simple example, so lets return the identify value
    return(identity(x))
}

## Lets use it now
myFun(1:10)

## Experienced user wants to print status reports
myFun(1:10, verbose = TRUE)

Ok, this is an example and there are really only two arguments but in practice things can get complicated fast. As a developer, soon you might find yourself with functions that have 10 or more arguments and need to cut the list down to promote friendliness and usability of your code.

Note that if you are using the same advanced argument inside several functions, you might want to consider using the function getOption(). It's useful for users who want to set global options and don't want to specify them for every single function call.

We highly recommend documenting any advanced arguments using roxygen2 tags inside the source code so experienced users can understand the code. We also recommend explaining these advanced arguments in your package vignette at the section targeting advanced users.

That is, we expect experienced users to be able to dig into either the source code and/or find the appropriate docs section. As a package developer you have to give them a place to find such documentation.

formal_call()

Once you start using the ... argument, you will soon find use cases where you are using ... for several functions, but then some of them might complain when ... contains an argument they don't use. In such scenarios you will want to run the function using some key arguments, remaining arguments from ... that the function was defined to use, and drop the unwanted arguments in ....

That's where formal_call() comes into play.

## Explore source
## Alias: formalCall()
formal_call

Basic example

The following example shows how you can use formal_call() inside your functions. First, lets try a simple function that will return the identity of the maximum value of a vector x.

myFunBroken <- function(x, ...) {
    identity(max(x, ...), ...)
}

## Test without using ...
myFunBroken(1:10)

That worked because we are not using the ... argument. However, as is the code will break when using ... because identity() does not have have other arguments defined.

## Breaks when you use ...
myFunBroken(1:10, 11:20)

We can fix the broken code by using formal_call() as shown below.

## Fix code using formal_call()
myFunFixed <- function(x, ...) {
    formal_call(identity, x = max(x, ...), ...)
}

## Check that we are getting the same value as before
identical(myFunBroken(1:10), myFunFixed(1:10))

## Doesn't break when you use ...
myFunFixed(1:10, 11:20)

In this particular example, you could dodge the problem by using identity(x) instead of identity(x, ...). However, the illustrative use of formal_call() remains and there are more complicated cases where you will need to use it.

Complex example

In the following example we want a function that does things with ... (in this case, it defines the verbose argument) and then pass it to plot(). Simply using dots() is not enough as plot() will complain that verbose is not a graphical parameter.

## A more complicated example
funkyPlot <- function(x, y, ...) {
    verbose <- dots('verbose', getOption('verbose', FALSE), ...)
    if(verbose)
        message(paste(Sys.time(), 'funkyPlot: getting ready to roll'))
    plot(x, y, ...)
}

## funkyPlot() doesn't work
tryCatch(funkyPlot(1:10, 10:1, verbose = TRUE, xlab = 'Data (units)'), warning = function(w) { print(w) })

There are many graphical parameters, but they are not part of the formal definition of plot(). We can solve this by using the advanced argument formalCallUse which specifies arguments we still want to use even if they are not part of the formal definition.

Below we do so only for the xlab graphical parameter.

## Use 'xlab'
funkyPlot2 <- function(x, y, ...) {
    verbose <- dots('verbose', getOption('verbose', FALSE), ...)
    if(verbose)
        message(paste(Sys.time(), 'funkyPlot: getting ready to roll'))
    xlab <- dots('xlab', '', ...)
    formal_call(plot, x = x, y = y, ..., formalCallUse = list(xlab = xlab))
}

## Works now =)
funkyPlot2(1:10, 10:1, verbose = TRUE, xlab = 'Data (units)')

If you have a small number of advanced arguments you want users to be able to use, this approach will work. But in the case of plot() there are tons of graphical parameters that we want users to be able to use.

If you knew for certain that the other functions inside yours only use a small number of arguments passed through ... (verbose in our example), you can eliminate them from ... yourself and pass them to formalCallUse.

## If we knew for sure that we only want to exclude 'verbose' from ... before
## calling plot() we can do so this way.
funkyPlot3 <- function(x, y, ...) {
    verbose <- dots('verbose', getOption('verbose', FALSE), ...)
    if(verbose)
        message(paste(Sys.time(), 'funkyPlot: getting ready to roll'))

    ## Drop 'verbose' from ...
    use <- list(...)
    use <- use[!names(use) == 'verbose']

    ## Call plot. Note that we are not passing ... anymore.
    formal_call(plot, x = x, y = y, formalCallUse = use)
}

## Works with more graphical parameters
funkyPlot3(1:10, 10:1, verbose = TRUE, xlab = 'Data (units)', ylab = 'Success', main = 'Complicated example')

So for these very complicated use cases, formalCallUse can help you when you either want to make sure you are supplying a small set of arguments or if you have a small set you want to exclude.

Cases where there are many arguments to either exclude or supply are nasty either way.

Learn more

If you want to check other examples using dots(), check the functions last() and recursive_last().

You can also see how these functions are used in packages such as derfinder: Biconductor page, GitHub code.

Reproducibility

Code for creating the vignette

## Create the vignette
library('knitrBootstrap') 

knitrBootstrapFlag <- packageVersion('knitrBootstrap') < '1.0.0'
if(knitrBootstrapFlag) {
    ## CRAN version
    library('knitrBootstrap')
    system.time(knit_bootstrap('dots.Rmd', chooser=c('boot', 'code'), show_code = TRUE))
    unlink('dots.md')
} else {
    ## GitHub version
    library('rmarkdown')
    system.time(render('dots.Rmd'))
}
unlink('dotsRef.bib')
## Note: if you prefer the knitr version use:
# library('rmarkdown')
# system.time(render('dots.Rmd', 'html_document'))

## Extract the R code
library('knitr')
knit('dots.Rmd', tangle = TRUE)

Date the vignette was generated.

## Date the vignette was generated
Sys.time()

Wallclock time spent generating the vignette.

## Processing time in seconds
totalTime <- diff(c(startTime, Sys.time()))
round(totalTime, digits=3)

R session information.

## Session info
library('devtools')
options(width = 90)
session_info()

Bibliography

This vignette was generated using knitrBootstrap r citep(bib[['knitrBootstrap']]) with knitr r citep(bib[['knitr']]) and rmarkdown r citep(bib[['rmarkdown']]) running behind the scenes.

Citations made with knitcitations r citep(bib[['knitcitations']]).

## Print bibliography
bibliography()


lcolladotor/dots documentation built on Aug. 24, 2019, 5:23 p.m.