Author: Mark van der Loo
Date: 2021-05-07

# 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.

- Define and manage option sets for your package or application with ease
- Create option managers that can validate user-defined options in a flexible way.
- Implement multiple levels of local and global options if so desired
- As a package author, you have the option to export a `reset` function, allowing your users to reset options to their defaults.

### 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:

- `foo` with default value `"up"` and allowed values `"up"` and `"down"`
- `bar` with default value 2 and `0 <= bar <= 3`.

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

- If you `depend` the package on `options`, then `reset` (and all other functions) are available immediately for the user (this is not recommended).
- If you `import` the package you have to export `reset` explicitly if you want to expose it. This can be done for example by

    #' 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