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