Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Enum-like arguments in R

Tags:

enums

r

I'm new to R and I'm currently trying to supply the enumeration-like argument to the R function (or the RC/R6 class method), I currently use character vector plus match.arg similar to the following:

EnumTest = function(enum = c("BLUE", "RED", "BLACK")) {
  enumArg <-
    switch(
      match.arg(enum), "BLUE" = 0L, "RED" = 1L, "BLACK" = 2L
    )
  switch(enumArg,
         # do something
  )
}

Is there are better/more concise way to imitate enum-like behavior in R? E.g. one big problem that user has to know the set of possible values for the argument and manually type it as a string - without any suggestion or auto-completion...

If there is no other better way, one thing that could improve above approach - it'd be nice to make it more concise by say predefining enums globally or say as private members of R6 class:

Color <- c("BLUE", "RED", "BLACK")

Then one could (re)use it in one or more function definitions, e.g.:

EnumTest = function(enum = Color) { 
...

However, I'm not sure how to use this Color vector in match.arg function. It'd be nice if I could define Color as a map with keys being actual color values and values being integer representation - but I'm not sure how sensible that is.. Anyways, maybe there are more common neat approaches exist.

The main goal would be to provide an easy-to-use intuitive interface to the user of my package and functions (e.g. easy way to find the set of possible values, tab-completion, auto-suggestion, etc..), followed by standardized development of such functions using enum-like arguments

like image 550
Oleg Shirokikh Avatar asked Nov 21 '15 00:11

Oleg Shirokikh


Video Answer


2 Answers

How about using a function that defines the enum by returning list(a= "a", ...)? You can then either assign the returned vector to a variable and use it in context, or use the function directly. Either a name or an integer reference will work as an index, although you have to use the unlist version of the index lookup, [[, otherwise you get a list with one element.

colorEnum <- function() {
    list(BLUE = "BLUE", RED = "RED", BLACK = "BLACK")
}

colorEnum()$BLUE
#> [1] "BLUE"
colorEnum()[[1]]
#> [1] "BLUE"
colorEnum()[1]
#> $BLUE
#> [1] "BLUE"

col <- colorEnum()
col$BLUE
#> [1] "BLUE"
col[[1]]
#> [1] "BLUE"
col$BAD_COLOR
#> NULL
col[[5]]
#> Error in col[[5]] : subscript out of bounds

You can get the list of names for use in a match, i.e. your function parameter could be

EnumTest = function( enum = names(colorEnum()) { ...

You can actually abbreviate too, but it must be unique. (If you use RStudio, since col is a list, it will suggest completions!)

col$BLA
#> [1] "BLACK"
col$BL
#> NULL

If you want more sophisticated enum handling, you could assign S3 classes to the thing returned by your enum constructor function and write a small collection of functions to dispatch on class "enum" and allow case-insensitive indexing. You could also add special functions to work with a specific class, e.g. "colorEnum"; I have not done that here. Inheritance means the list access methods all still work.

colorEnum2 <- function() {
    structure(
        list(BLUE = "BLUE", RED = "RED", BLACK = "BLACK"),
        class= c("colorEnum2", "enum", "list")
    )
}

# Note, changed example to allow multiple returned values.
`[.enum` <- function(x, i) {
    if ( is.character( i ))
        i <- toupper(i)
    class(x) <- "list"
    names(as.list(x)[i])
}

`[[.enum` <- function(x, i, exact= FALSE) {
    if ( is.character( i ))
        i <- toupper(i)
    class(x) <- "list"
    as.list(x)[[i, exact=exact]]
}

`$.enum` <- function(x, name) {
    x[[name]]
}

col <- colorEnum2()
# All these return [1] "RED"
col$red
col$r
col[["red"]]
col[["r"]]
col["red"]

col[c("red", "BLUE")]
#> [1] "RED" "BLUE"

col["r"]
[1] NA   # R does not matches partial strings with "["

These override the built in [, [[ and $ functions when the thing being indexed is of class "enum", for any "enum" classed objects. If you need another one, you just need to define it.

 directionEnum <- function() {
    structure(
        list(LEFT = "LEFT", RIGHT = "RIGHT"),
        class= c("directionEnum", "enum", "list")
    )
}

directionEnum()$l
#> [1] "LEFT"

If you need several enum objects, you could add a factory function enum that takes a vector of strings and a name and returns an enum object. Most of this is just validation.

enum <- function(enums, name= NULL) {
    if (length(enums) < 1)
        stop ("Enums may not be empty." )
    enums <- toupper(as.character(enums))
    uniqueEnums <- unique(enums)
    if ( ! identical( enums, uniqueEnums ))
        stop ("Enums must be unique (ignoring case)." )
    validNames <- make.names(enums)
    if ( ! identical( enums, validNames ))
       stop( "Enums must be valid R identifiers." )

    enumClass <- c(name, "enum", "list")
    obj <- as.list(enums)
    names(obj) <- enums
    structure( obj, class= enumClass)
}

col <- enum(c("BLUE", "red", "Black"), name = "TheColors")
col$R
#> [1] "RED"
class(col)
#> [1] "TheColors" "enum"      "list"

side <- enum(c("left", "right"))
side$L
#> [1] "LEFT"
class(side)
#> [1] "enum" "list"

But now this is starting to look like a package...

like image 177
Stuart R. Jefferys Avatar answered Oct 10 '22 01:10

Stuart R. Jefferys


I like to use environments as replacement for enums because you can lock them to prevent any changes after creation. I define my creation function like this:

Enum <- function(...) {

  ## EDIT: use solution provided in comments to capture the arguments
  values <- sapply(match.call(expand.dots = TRUE)[-1L], deparse)

  stopifnot(identical(unique(values), values))

  res <- setNames(seq_along(values), values)
  res <- as.environment(as.list(res))
  lockEnvironment(res, bindings = TRUE)
  res
}

Create a new enum like this:

FRUITS <- Enum(APPLE, BANANA, MELON)

We can the access the values:

FRUITS$APPLE

But we cannot modify them or create new ones:

FRUITS$APPLE <- 99  # gives error
FRUITS$NEW <- 88  # gives error
like image 13
AEF Avatar answered Oct 10 '22 01:10

AEF