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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With