rr6

Do active bindings need to return self?


In Hadley's Advanced R (2nd ed) book (https://adv-r.hadley.nz/r6.html#active-fields) we see the definition of the name active binding for the R6 Person class. When the name is being set with this binding, the function returns self (and not invisibly). Earlier, the argument to return self invisibly is so that methods can be chained. As I am unaware that bindings can be chained, is this addition not necessary?

Person <- R6Class("Person", 

  private = list(
    .age = NA,
    .name = NULL
  ),
  active = list(
    age = function(value) {
      if (missing(value)) {
        private$.age
      } else {
        stop("`$age` is read only", call. = FALSE)
      }
    },
    name = function(value) {
      if (missing(value)) {
        private$.name
      } else {
        stopifnot(is.character(value), length(value) == 1)
        private$.name <- value
        self
      }
    }
  ),
  public = list(
    initialize = function(name, age = NA) {
      private$.name <- name
      private$.age <- age
    }
  )
)

Solution

  • Your comment-question:

    Why is the self (or any other return value) ignored in active bindings

    I think we can generalize this to say

    Why can we not change the value returned by an assignment?

    The issue is that during assignment into an R6 property, an R6 active-binding, or either in a non-R6 object, we're using R's primitive function <-, and its behavior is set: it always returns the RHS (value) of the assignment operation, not the LHS (object). This is also true of classed assignments such as $<-.superlist, we are still going through <-.

    G <- list()
    class(G) <- c("superlist", "list")
    `$<-.superlist` <- function(obj, name, value) { obj[[name]] <- value; obj; }
    (G$abc <- 99L)
    # [1] 99
    (G$xyz <- "a")
    # [1] "a"
    G
    # $abc
    # [1] 99
    # $xyz
    # [1] "a"
    # attr(,"class")
    # [1] "superlist" "list"     
    

    If we were to debug `$<-.superlist`, we would see that the unparsed obj is a (magic?) symbol *tmp* (I think this reflects R's copy-on-write semantics this is just a temporary name to prevent double-evaluation, thanks to @KonradRudolph for pointing that out), and no matter what we do with this function, it always returns value. To change that behavior would be to change the assignment operation in everything in R (which might have ... consequences ...).

    This suggests that if we wanted to return the LHS of an assignment operator, we'd have to use our own assignment operator, avoiding <- (and =).

    The closest I could get to that (alternative assignment operator) is a "setter" method of the Person object that does what the self in the $name active-binding suggests (chaining):

    Person <- R6Class("Person", 
      private = list(
        .age = NA,
        .name = NULL
      ),
      active = list(
        age = function(value) {
          if (missing(value)) {
            private$.age
          } else {
            stop("`$age` is read only", call. = FALSE)
          }
        },
        name = function(value) {
          if (missing(value)) {
            private$.name
          } else {
            stopifnot(is.character(value), length(value) == 1)
            private$.name <- value
            self
          }
        }
      ),
      public = list(
        initialize = function(name, age = NA) {
          private$.name <- name
          private$.age <- age
        },
        name2 = function(value) {                                  ### NEW
          if (missing(value)) {
            private$.name 
          } else {
            stopifnot(is.character(value), length(value) == 1)
            private$.name <- value
            self
          }
        }
      )
    )
    
    ted <- Person$new("Ted", 38)
    ted$name2("Fred")$name2("Harry")
    # <Person>
    #   Public:
    #     age: active binding
    #     clone: function (deep = FALSE) 
    #     initialize: function (name, age = NA) 
    #     name: active binding
    #     name2: function (value) 
    #   Private:
    #     .age: 38
    #     .name: Harry
    

    This works because the method $name2() is a function we have full control over. It is not directly using the return-value from <- or =.

    Unsatisfyingly, "why it is so" gets the reply "because that is how it is". That's how R is designed, and it is a primitive expectation that permeates most (all?) of R. Sorry, perhaps not a happy fulfilling answer, it just "is". (And it has nothing personal to do with active-bindings or R6.)

    P.S.: I think the adv-r article certainly suggests that an active-binding can return self or anything other than its value. It is possibly (as already suggested) a copy/paste oversight on the author's part. I understand your interpretation of it.