Table of Contents

Generate Global Options

Author: Zuguang Gu ( z.gu@dkfz.de )

Date: 2018-06-06


Global option function such as options() and par() provides a way to control global settings. Here the GlobalOptions package provides a more general and controlable way to generate such functions, which can:

  1. validate the values (e.g. class, length and self-defined validations);
  2. set read-only options;
  3. set invisible options;
  4. set private options which are only accessable in a certain namespace.
  5. support local options and global options

General usage

The most simple use is to generate an option function with default values by callling setGlobalOptions() or its short versoin set_opt():

library(GlobalOptions)
opt = set_opt(
    a = 1,
    b = "text"
)

The returned value opt is an option function which can be used to get or set options. Options in opt can be accessed either by specifying as arguments or by using the $ operator.

opt()
## $a
## [1] 1
## 
## $b
## [1] "text"
opt("a")
## [1] 1
opt$a
## [1] 1
op = opt()
op
## $a
## [1] 1
## 
## $b
## [1] "text"
opt(a = 2, b = "new text")
opt()
## $a
## [1] 2
## 
## $b
## [1] "new text"
opt$b = ""
opt()
## $a
## [1] 2
## 
## $b
## [1] ""
opt(op)
opt()
## $a
## [1] 1
## 
## $b
## [1] "text"

opt generated by set_opt() contains an argument RESET which is used to reset the options to the default:

opt(a = 2, b = "new text")
opt(RESET = TRUE)
opt()
## $a
## [1] 1
## 
## $b
## [1] "text"

Simply printing opt gives a summary of all options.

opt
##  Option Value
##       a     1
##       b  text

Advanced usage

If option values are set as lists, more configurations can be customized.

Simple validation

There are two basic fields that are used to check the input option values:

opt = set_opt(
    a = list(.value = 1,
             .length = c(1, 3),
             .class = "numeric")
)

In above code, .value is the default value for the option a. The length of the value is controlled by .length and the length should be either 1 or 3. The class of the value should be numeric. If the input value does not fit these criterions, there will be an error. The value of .length or .class is a vector and the checking will be passed if one of the value fits user's input.

opt(a = 1:2)  # there will be error because the length is 2
## Error: Length of 'a' should be one of 1, 3.
opt(a = "text")  # there will be error because the input is character
## Error: Class of 'a' should be 'numeric'.

Read-only options

The value can be set as read-only by .read.only field and modifying such option will cause an error.

opt = set_opt(
    a = list(.value = 1,
             .read.only = TRUE)
)
opt(a = 2)  # there will be error because a is read-only
## Error: 'a' is a read-only option.

There is also a pre-defined argument READ.ONLY in opt() which controls whether to return only the read-only options or not.

opt = set_opt(
    a = list(.value = 1,
             .read.only = TRUE),
    b = 2
)
opt(READ.ONLY = TRUE)
## $a
## [1] 1
opt(READ.ONLY = FALSE)
## $b
## [1] 2
opt(READ.ONLY = NULL)  # default, to return both
## $a
## [1] 1
## 
## $b
## [1] 2

User-defined validation

More customized validation of the option values can be controlled by .validate field. The value of .validate should be a function. The input of the validation function is the input option value and the function should only return a logical value.

a should only between 0 and 10 in following example.

opt = set_opt(
    a = list(.value = 1,
             .validate = function(x) x > 0 && x < 10
    )
)
opt(a = 20)  # This will cause an error
## Error: a didn't pass the validation. Your option is invalid.

.failed_msg is used to configure the error message once validation is failed.

opt = set_opt(
    a = list(.value = 1,
             .validate = function(x) x > 0 && x < 10,
             .failed_msg = "'a' should be in (0, 10)."
    )
)
opt(a = 20)  # This will cause an error
## Error: a didn't pass the validation. 'a' should be in (0, 10).

Filter the option values

Filtering on the option values can be controlled by .filter field. This is useful when the input option value is not valid but it is not necessary to throw errors. More proper way is to modify the value silently. For example, there is an option to control whether to print messages or not and it should be set to TRUE or FALSE. However, users may set some other type of values such as NULL or NA. In this case, non-TRUE values can be converted to logical values by .filter. Similar as .validate, the input value for filter function is the input option value, and it should return a filtered option value.

opt = set_opt(
    verbose = 
        list(.value = TRUE,
             .filter = function(x) {
                 if(is.null(x)) {
                     return(FALSE)
                 } else if(is.na(x)) {
                     return(FALSE)
                 } else {
                     return(x)
                 }
              })
)
opt(verbose = FALSE); opt("verbose")
## [1] FALSE
opt(verbose = NA); opt("verbose")
## [1] FALSE
opt(verbose = NULL); opt("verbose")
## [1] FALSE

Another example is when there is an option which controls four margin values of a plot, the length of the value can either be 1, 2, or 4. With .filter, length can be normaliezd to 4 consistently.

opt = set_opt(
    margin = 
        list(.value = c(1, 1, 1, 1),
             .length = c(1, 2, 4),
             .filter = function(x) {
                if(length(x) == 1) {
                    return(rep(x, 4))
                } else if(length(x) == 2) {
                    return(rep(x, 2))
                } else {
                    return(x)
                }
            })
)
opt(margin = 2); opt("margin")
## [1] 2 2 2 2
opt(margin = c(2, 4)); opt("margin")
## [1] 2 4 2 4

Dynamic querying the option value

The input option value can be set dynamicly by setting it as a function. When the option value is set as a function and class of the option is non-function, it will be executed when querying the option. In the following example, the prefix option corresponds to the prefix of log messages. The returned option value is the string after the execution of the input function.

opt = set_opt(
    prefix = ""
)
opt(prefix = function() paste("[", Sys.time(), "] ", sep = " "))
opt("prefix")  # or opt$prefix
## [1] "[ 2018-06-06 10:28:51 ] "
Sys.sleep(2)
opt("prefix")
## [1] "[ 2018-06-06 10:28:53 ] "

If the value of the option is a real function and users don't want to execute it, just set .class to contain function, then the function will be treated as a simple value.

opt = set_opt(
    test_fun = list(.value = function(x1, x2) t.test(x1, x2)$p.value,
                    .class = "function")
)
opt(test_fun = function(x1, x2) cor.test(x1, x2)$p.value)
opt("test_fun")  # or opt$test_fun
## function(x1, x2) cor.test(x1, x2)$p.value

Interaction between options

The self-defined function (i.e. value function, validation function or filter function) is applied per-option independently. But sometimes we want to set one option based on values of other options. In this case, we need a function which can get other option values. .v() can be used to access other option values defined beforehand. .v("a") can also be written as .v(a) or .v$a.

opt = set_opt(
    a = 1,
    b = function() 2 * .v$a
)
opt("b")  # or opt$b
## [1] 2
opt(a = 2)
opt("b")
## [1] 4

However, you can still overwrite option b:

opt(a = 2, b = 3) # b was overwriiten and will not be 2*a
opt()
## $a
## [1] 2
## 
## $b
## [1] 3

.v can also be used in .validate and .filter fields. In the second example, sign of b should be as same as sign of a.

opt = set_opt(
    a = 1,
    b = list(.value = 0,
             .validate = function(x) {
                 if(.v$a > 0) x > 0
                 else x < 0
             },
             .filter = function(x) {
                 x + .v$a
             },
             .failed_msg = "'b' should have same sign as 'a'.")
)
opt(b = 1)
opt("b")
## [1] 2
opt(a = 1, b = -1)  # this should cause an error
## Error: b didn't pass the validation. 'b' should have same sign as 'a'.

Local options

The option funtion also has a LOCAL argument which switches local mode and global mode. When LOCAL is set to TRUE, a copy of current options is generated and all queries are applied on the copy version. The local mode is turned off when LOCAL is explicitely specified to FALSE.

opt = set_opt(
    a = 1
)

opt(LOCAL = TRUE)
opt(a = 2)
opt$a
## [1] 2
opt(LOCAL = FALSE)
opt$a
## [1] 1

Local mode will be automatically turned off when enrivonment changes. In following example, local mode only works inside f1() and f2() functions and the local copies are independent in f1() and f2(). Note when leaving e.g. f1(), the copy of the option is deleted.

opt = set_opt(
    a = 1
)

f1 = function() {
    opt(LOCAL = TRUE)
    opt(a = 2)
    return(opt$a)
}
f1()
## [1] 2
opt$a
## [1] 1
f2 = function() {
    opt(LOCAL = TRUE)
    opt(a = 4)
    return(opt$a)
}
f2()
## [1] 4
opt$a
## [1] 1

If f1() calls f2(), f2() will be in the same local mode as f1(). In other word, all children frames are in a same local mode if the parent frame is in local mode.

opt = set_opt(
    a = 1
)

f1 = function() {
    opt(LOCAL = TRUE)
    opt(a = 2)
    return(f2())
}

f2 = function() {
    opt$a
}

f1()
## [1] 2
opt$a
## [1] 1

Synonymous options

It can be possible that several weeks later, developers have better names for the options. They want to use the new option names but still do not want to disable the old ones. In this case, .synonymous field can be set to let the new option and old option reference to a same internal option object (which means all other configuration specified for this option is ignored). The change of values of either one will also affect the companions correspondingly.

opt = set_opt(
    old = 1,
    new = list(.value = 1,
               .synonymous = "old")
)
opt()
## $old
## [1] 1
## 
## $new
## [1] 1
opt$old = 2
opt()
## $old
## [1] 2
## 
## $new
## [1] 2
opt$new = 3
opt()
## $old
## [1] 3
## 
## $new
## [1] 3

Print the object

There is a .description field for each option which is only used when printing the summary of options. As shown before, simply entering the option object gives a summary table for all options.

opt = set_opt(
    a = 1,
    b = "b",
    c = list(.value = letters[1:4],
             .class = "character",
             .description = "26 letters"),
    d = list(.value = c(0, 0),
             .class = "numeric",
             .validate = function(x) x[1]^2 + x[2]^2 <= 1,
             .failed_msg = "The point should be in the unit circle",
             .description = "start points in the unit circle"),
    e = list(.value = rnorm,
             .class = "function",
             .description = "distribution to generate random numbers")
)
opt
##  Option                   Value                             Description
##       a                       1                                      ""
##       b                       b                                      ""
##       c              a, b, c, d                              26 letters
##       d                    0, 0         start points in the unit circle
##       e a user-defined function distribution to generate random numbers

Use dump_opt() to get summary for each option.

dump_opt(opt, "a")
##                    Field                   Value
##                     name                       a
##            default_value                       1
##            current_value                       1
##                   length                no limit
##                    class                no limit
##                 validate a user-defined function
##               failed_msg Your option is invalid.
##                   filter a user-defined function
##                read.only                   FALSE
##                  private                   FALSE
##                  visible                    TRUE
##              description                      ""
##  __generated_namespace__             R_GlobalEnv
dump_opt(opt, "d")
##                    Field                                  Value
##                     name                                      d
##            default_value                                   0, 0
##            current_value                                   0, 0
##                   length                               no limit
##                    class                                numeric
##                 validate                a user-defined function
##               failed_msg The point should be in the unit circle
##                   filter                a user-defined function
##                read.only                                  FALSE
##                  private                                  FALSE
##                  visible                                   TRUE
##              description        start points in the unit circle
##  __generated_namespace__                            R_GlobalEnv

Add new options

New options can be added after the option function is created by explicitely specifying ADD = TRUE:

opt = set_opt(a = 1)
opt(b = 2, ADD = TRUE)
opt
##  Option Value
##       a     1
##       b     2

Note you cannot add new options by using $ (or more precisely $<-) operator because $ can only access options that have already been created.

opt$c = 3
## Error: No such option: 'c'. If you want to add this new option, please use your_opt_fun(c = ...,
## ADD = TRUE)

Like using a complex configuration list when creating a new option in set_opt(), here you can also use configuration list with ADD = TRUE.

opt(c = list(.value = "c", 
             .class = "character"), 
    ADD = TRUE)
opt
##  Option Value
##       a     1
##       b     2
##       c     c
opt$c = 1
## Error: Class of 'c' should be 'character'.

Features for package development

Two additional fields may be helpful when developing packages. .visible controls whether options are visible to users. The invisible option can only be queried or modified by specifying its option name (just like you can only open the door with the correct unique key). This would be helpful if users want to put some secret options while do not want others to access. Is this case, they can assign names with complex strings like .__MY_PRIVATE_KEY__. as their secret options and afterwards they can access it with this special key.

opt = set_opt(
    a = list(.value = 1,
             .visible = FALSE),
    b = 2
)
opt()
## $b
## [1] 2
opt$a
## [1] 1
opt$a = 2
opt$a
## [1] 2
opt()
## $b
## [1] 2

Another field .private controls whether the option is only private to the namespace (e.g. packages). If it is set to TRUE, the option can only be modified in the same namespace (or top environment) where the option function is generated. E.g, if you are writing a package named foo and generating an option function foo_opt(), by setting the option with .private to TRUE, the value for such options can only be modified inside foo package while it is not permitted outside foo. At the same time, private options become read-only options if querying outside foo package.

In following example, we manually modify the namespace where set_opt() is called in stats package.

opt = set_opt(
    a = list(.value = 1,
             .private = TRUE)
)
require(stats)
ns = getNamespace("stats")
environment(opt)$options$a$`__generated_namespace__` = ns

There will be error if trying to modify a which is private in stats namespace.

opt$a = 2
## Error: 'a' is a private option and it can only be modified inside 'stats' namespace while not
## 'R_GlobalEnv'.

But you can still access it.

opt$a
## [1] 1

The option object generated by set_opt() is actually a function. It contains four arguments: ..., RESET, READ.ONLY, LOCAL, ADD. If you want to put the option function into a package, remember to document all the four arguments:

args(opt)
## function (..., RESET = FALSE, READ.ONLY = NULL, LOCAL = FALSE, 
##     ADD = FALSE) 
## NULL

Misc

The order of validation when modifying an option value is .read.only, .private, .length, .class, .validate, .filter, .length, .class. Note validation on length and class of the option values will be applied again after filtering.

Global options are stored in private environments. Each time when generating a option function, there will be new environments created. Thus global options will not conflict if they come from different option functions.

opt1 = set_opt(
    a = list(.value = 1)
)
opt2 = set_opt(
    a = list(.value = 1)
)
opt1$a = 2
opt1$a
## [1] 2
opt2$a
## [1] 1

Note the option values can also be set as a list, so for the list containing configurations, names of the field is started with a dot . to be distinguished from the normal list.

opt = set_opt(
    list = list(a = 1,
                b = 2)
)
opt()
## $list
## $list$a
## [1] 1
## 
## $list$b
## [1] 2
opt = set_opt(
    list = list(.value = list(a = 1, b = 2),
                .class = "list")
)
opt()
## $list
## $list$a
## [1] 1
## 
## $list$b
## [1] 2
opt$list = 1  # this will cause an error
## Error: Class of 'list' should be 'list'.

If you made a type of the field names when configurating the options (e.g. forgot to type the leading dot), there will be a warning and the whole configuration list is treated as a normal list for this option.

opt = set_opt(
  a = list(.value = 1,
           class = "numeric")  # <- here it should be .class
)
## Warning: Your definition for 'a' is mixed. It should only contain .value, .class, .length,
## .validate, .failed_msg, .filter, .read.only, .private, .visible, .synonymous,
## .description. Ignore the setting and use the whole list as the default value.
opt$a
## $.value
## [1] 1
## 
## $class
## [1] "numeric"

The final and the most important thing is the validation by .class, .length, .validate, .filter will not be applied on default values because users who design their option functions should know whether the default values are valid or not.

opt = set_opt(
    a = list(.value = -1,
             .validate = function(x) x > 0)
)
opt$a
## [1] -1

Session info

sessionInfo()
## R version 3.4.4 (2018-03-15)
## Platform: x86_64-apple-darwin15.6.0 (64-bit)
## Running under: macOS High Sierra 10.13.2
## 
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/3.4/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/3.4/Resources/lib/libRlapack.dylib
## 
## locale:
## [1] C/en_GB.UTF-8/en_GB.UTF-8/C/en_GB.UTF-8/en_GB.UTF-8
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] GlobalOptions_0.1.0 knitr_1.20          markdown_0.8       
## 
## loaded via a namespace (and not attached):
## [1] compiler_3.4.4  magrittr_1.5    tools_3.4.4     stringi_1.2.2   stringr_1.3.1   evaluate_0.10.1