Introduction to settings

Mark van der Loo

2021-05-07

What are options?

Software options are usually simple strings or numbers that alter the behaviour of a programme, package or functions. Many options in R can be set with the standard options functions. However, sometimes it is convenient to have richer option management facilities.

What does the settings package do?

The settings package is aimed to bring easy-to-use, yet powerful options management to R. It is light-weight, consists of only a few-hundred lines of code and does not import anything that does not come with a standard R installation.

Basic option settings management

To start storing options we first define an options manager by feeding it default option names and values.

my_options <- options_manager(foo = 1, bar = 2, baz = 'hello')
my_options()
#> $foo
#> [1] 1
#> 
#> $bar
#> [1] 2
#> 
#> $baz
#> [1] "hello"

You may pass an arbitrary number of such [key]=[value] pairs. If you want to define an option without a default value, just set its value to NULL.

Individual values can be set and retrieved using the function my_options that we’ve just created.

my_options('foo')
#> [1] 1
my_options('foo','baz')
#> $foo
#> [1] 1
#> 
#> $baz
#> [1] "hello"

Option values may also be set as follows.

my_options(foo=7)
my_options()
#> $foo
#> [1] 7
#> 
#> $bar
#> [1] 2
#> 
#> $baz
#> [1] "hello"
# or multiple options at once
my_options(foo=7,bar=0)
my_options()
#> $foo
#> [1] 7
#> 
#> $bar
#> [1] 0
#> 
#> $baz
#> [1] "hello"

And we reset everything to factory settings.

reset(my_options)
my_options()
#> $foo
#> [1] 1
#> 
#> $bar
#> [1] 2
#> 
#> $baz
#> [1] "hello"

Limiting options

It is possible to limit the option values a user can set so you don’t have to check them at run time.

opt <- options_manager(foo="up", bar=2
  , .allowed = list(
      foo = inlist("up","down")
    , bar = inrange(min=0, max=3)
  )
)

In the above code, we set the following options:

If you try to set an invalid option, an error is produced.

> opt(foo="middle")
Error: Option value out of range. Allowed values are up, down
> opt(bar=7)
Error: Option value out of range. Allowed values are in [0, 3]

You don’t need to set allowed values or ranges for each and every option. Only those options that have an entry in the .allowed list will be checked.

It is possible to customize the way options are checked or manipulated. This is explained in more detail in a second vignette.

Global versus local options

It is nice when the behaviour of a function that depends on global options can be altered at function call. With the settings package you can create local options as follows. First, we create a global options manager.

my_options <- options_manager(a=2,b=3)

The following function uses global settings by default, but a user can overwrite them by passing extra options as [name]=[value] pairs.

f <- function(x,...){
  # create local copy of options, merged with the global options.
  local_opts <- clone_and_merge(my_options,...)
  # local options can be used
  local_opts('a') + local_opts('b') * x 
}

Now compare the following uses.

# a and b are taken from global option set.
f(1)         # 2 + 3 * 1
#> [1] 5
# specify 'a'
f(1,a=10)    # 10 + 3 * 1
#> [1] 13
#specify 'a' and 'b'
f(1,a=10,b=100) # 10 + 100 * 1
#> [1] 110

# global options are unaltered, as expected.
my_options()
#> $a
#> [1] 2
#> 
#> $b
#> [1] 3

Note: the reset function may also be used to reset options in local_opts within the definiton of f. This will not affect the global options.

Using the settings package as options manager for your package.

The easiest way is probably to create a file for example called options.R. Here’s an example.

# Variable, global to package's namespace. 
# This function is not exported to user space and does not need to be documented.
MYPKGOPTIONS <- options_manager(a=1, b=2)

# User function that gets exported:

#' Set or get options for my package
#' 
#' @param ... Option names to retrieve option values or \code{[key]=[value]} pairs to set options.
#'
#' @section Supported options:
#' The following options are supported
#' \itemize{
#'  \item{\code{a}}{(\code{numeric};1) The value of a }
#'  \item{\code{b}}{(\code{numeric};2) The value of b }
#' }
#'
#' @export
pkg_options <- function(...){
  # protect against the use of reserved words.
  stop_if_reserved(...)
  MYPKGOPTIONS(...)
}

Here, we’ve introduced a new function called stop_if_reserved That is because a few words are for the options package’s internal use, see the documentation of stop_if_reserved for the list. All reserved words start with .__ (dot-underscore-underscore) so the chance that a user tries to use them is probably small. However, it’s always good to be on the safe side.

Notes

#' Reset global options for pkg
#'
#' @export
pkg_reset() reset(MYPKGOPTIONS)

An example S4 class with local options and global default

First we define a general options manager. If this is part of a package, this will in general be invisible to the user.

# general options manager, will be invisible to user.
opt <- options_manager(foo=1,bar=2)

Now, define a class where the prototype contains the global settings.

# class definition containing default options in prototype.
TestClass <- setClass("TestClass"
  , slots=list(options='function',value='numeric')
  , prototype = list(
     options = opt
     , value = 0
    )
)

Note that a adding a function to an object is really adding a reference (since each function has its own environment, which is a reference object). For every instance of TestClass where the options slot is the default, a call to @options is a call to the global opt.

Now, we define a user-facing function that can set or get options, eiter globally or specific to an instance of TestClass.

setGeneric("test_options",function(where=NULL,...) standardGeneric("test_options"))
#> [1] "test_options"

# method for accessing global options
setMethod("test_options","ANY",function(where=NULL,...){
  do.call(opt,c(where,list(...)))
})

# method for getting/setting functions in a slot.
setMethod("test_options","TestClass", function(where=NULL,...){
  if (is_setting(...)){
    where@options <- clone_and_merge(where@options,...)
    where
  } else {
    where@options(...)
  }
})

There are two things to note here. First of all we’ve introduced the utility function is_setting which determines if the arguments in ... are meant to set options (TRUE) or to get them (FALSE). Secondly, note that for the ANY method, we need to merge the value of the first argument.

Now let’s see how it all works out.

# instantiate a class; with global options as currently set.
test <- TestClass()

# get global options
test_options()
#> $foo
#> [1] 1
#> 
#> $bar
#> [1] 2

# set a global option
test_options(foo=2)
test_options('foo')
#> [1] 2
# check that 'test' uses global option
test_options(test)
#> $foo
#> [1] 2
#> 
#> $bar
#> [1] 2

# set local option
test <- test_options(test,bar=3)
test_options(test)
#> $foo
#> [1] 2
#> 
#> $bar
#> [1] 3
# check global option
test_options()
#> $foo
#> [1] 2
#> 
#> $bar
#> [1] 2

An example Reference class with local options and global default

Again, we start by defining an options manager for the global scope.

opt <- options_manager(foo=1,bar=2)

The below reference class holds by default a reference to this manager.

RefTest <- setRefClass("RefTest"
  , fields =  list(.options='function',value='numeric')
  , methods = list(
    initialize = function(){
      .self$.options <- opt
      .self$value <- 0
    }
    , options = function(...){
        if(is_setting(...)){
          .self$.options <- clone_and_merge(.self$.options,...)
        } else {
          .self$.options(...)
        }
      }
    , reset = function(){
        # explicitly reference the 'settings' package here to avoid recursion.
        settings::reset(.self$.options) 
    }
    )
)

Note that we store the options in a field as if it was data, and not a method, so we can manipulate it with RefTest internal methods. Here’s how it functions.

reftest <- RefTest()

reftest$options()
#> $foo
#> [1] 1
#> 
#> $bar
#> [1] 2

# set global options
opt(foo=10)
reftest$options()
#> $foo
#> [1] 10
#> 
#> $bar
#> [1] 2

# set local options
reftest$options(bar=3)
reftest$options()
#> $foo
#> [1] 10
#> 
#> $bar
#> [1] 3
opt()
#> $foo
#> [1] 10
#> 
#> $bar
#> [1] 2

# reset local options
reftest$reset()
reftest$options()
#> $foo
#> [1] 1
#> 
#> $bar
#> [1] 2