Enum-like arguments in R

前端 未结 5 1465
我在风中等你
我在风中等你 2021-01-04 04:47

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

相关标签:
5条回答
  • 2021-01-04 05:05

    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)
    
    0 讨论(0)
  • 2021-01-04 05:09

    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
    
    0 讨论(0)
  • 2021-01-04 05:11

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

    0 讨论(0)
  • 2021-01-04 05:15

    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
    
    0 讨论(0)
  • 2021-01-04 05:16

    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

    • automatic translation of enum item names to the corresponding integer values
    • code auto completion (e. g. in RStudio)
    • clear documentation in the function signature which values are allowed as actual function parameters
    • easy validation of the actual function parameter against the allowed (integer) enum item values

    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 
    
    0 讨论(0)
提交回复
热议问题