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 ma
I just faced this exact problem and could only find this SO question. The objectProperties package mention by Paul seems abandoned (it produces several warnings) and has lots of overhead for such a simple (in principle) problem. I came up with the following lightweight solution (depends only on the stringi
package), which reproduces the feel of Enums in C languages. Maybe this helps someone.
EnumTest <- function(colorEnum = ColorEnum$BLUE) {
enumArg <- as.character(match.call()[2])
match.arg(enumArg, stringi::stri_c("ColorEnum$", names(ColorEnum)))
sprintf("%s: %i",enumArg,colorEnum)
}
ColorEnum <- list(BLUE = 0L, RED = 1L, BLACK = 2L)
Here is a simple method which supports enums with assigned values or which use the name as the value by default:
makeEnum <- function(inputList) {
myEnum <- as.list(inputList)
enumNames <- names(myEnum)
if (is.null(enumNames)) {
names(myEnum) <- myEnum
} else if ("" %in% enumNames) {
stop("The inputList has some but not all names assigned. They must be all assigned or none assigned")
}
return(myEnum)
}
If you are simply trying to make a defined list of names and don't care about the values you can use like this:
colors <- makeEnum(c("red", "green", "blue"))
If you wish, you can specify the values:
hexColors <- makeEnum(c(red="#FF0000", green="#00FF00", blue="#0000FF"))
In either case it is easy to access the enum names because of code completion:
> hexColors$green
[1] "#00FF00"
To check if a variable is a value in your enum you can check like this:
> param <- hexColors$green
> param %in% hexColors
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...
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
Update 07/21/2017: I have created a package for enumerations in R:
https://github.com/aryoda/R_enumerations
If you want to use self-defined enum-alike data types as arguments of R functions that support
you can define your own match.enum.arg
function, e. g.:
match.enum.arg <- function(arg, choices) {
if (missing(choices)) {
formal.args <- formals(sys.function(sys.parent()))
choices <- eval(formal.args[[as.character(substitute(arg))]])
}
if(identical(arg, choices))
arg <- choices[[1]][1] # choose the first value of the first list item
allowed.values <- sapply(choices,function(item) {item[1]}) # extract the integer values of the enum items
if(!is.element(arg, allowed.values))
stop(paste("'arg' must be one of the values in the 'choices' list:", paste(allowed.values, collapse = ", ")))
return(arg)
}
Usage:
You can then define and use your own enums like this:
ColorEnum <- list(BLUE = 1L, RED = 2L, BLACK = 3L)
color2code = function(enum = ColorEnum) {
i <- match.enum.arg(enum)
return(i)
}
Example calls:
> color2code(ColorEnum$RED) # use a value from the enum (with auto completion support)
[1] 2
> color2code() # takes the first color of the ColorEnum
[1] 1
> color2code(3) # an integer enum value (dirty, just for demonstration)
[1] 3
> color2code(4) # an invalid number
Error in match.enum.arg(enum) :
'arg' must be one of the values in the 'choices' list: 1, 2, 3