r-lib / R6

Encapsulated object-oriented programming for R
https://R6.r-lib.org
Other
404 stars 56 forks source link

Indexing methods #153

Open ifellows opened 5 years ago

ifellows commented 5 years ago

I recently wanted to implement indexing for some R6 classes and see that others are needing something similar. It doesn't look like there is an easy built in way to do this, but it looks like it could be added without too much effort.

`[.R6` <- function(x, ...) x$`[`(...) 
`[<-.R6` <- function(x, ...) x$`[<-`(...) 

Which allows

library(R6)

Foo = R6::R6Class(
    'Foo',
    public = list(
        X = NULL,
        metadata = NULL,
        initialize = function(X, metadata){
            self$X <- X
            self$metadata <- metadata
        },
        `[` = function(i, j){
            subfoo <- Foo$new(X = self$X[i, j, drop=FALSE], 
                             metadata = self$metadata)
            return(subfoo)
        }
    )
)

X <- matrix(1:8, ncol = 2)
foo <- Foo$new(X, 'blah blah')
foo[1:2,]

Is this something it makes sense to have in the package?

As an aside, overriding [[ wouldn't be good due to clashes with the existing environment dispatch. Is there any technical way to override ( so that objects could be functors?

wch commented 5 years ago

I can understand the desire to have a [ and [<- method for certain cases, but I feel that it's just kind of weird to make that a default for R6 classes in general.

I agree that overriding [[ is a bad idea because it would change existing behavior.

I don't think there's a way to override ( in R -- that is, only functions can use (. However, it is possible to create a function that has an object attached as an attribute, and override $ so that it accesses the object. For example:

library(R6)

# This takes an object with a $call() method
make_functor <- function(obj) {
  structure(
    function(...) {
      obj$call(...)
    },
    class = "functor",
    obj = obj
  )
}

`$.functor` <- function(x, name) {
  attr(x, "obj", exact = TRUE)[[name]]
}

`$<-.functor` <- function(x, name, value) {
  obj <- attr(x, "obj", exact = TRUE)
  obj[[name]] <- value
  # This function requires obj to be a reference object.
  # It could work with non-ref objects by adding `attr(x, "obj") <- obj` here.
  x
}

`[[.functor` <- `$.functor`
`[[<-.functor` <- `$<-.functor`

MyClass <- R6Class("MyClass",
  public = list(
    x = 1,
    call = function(n) {
      n + self$x
    }
  )
)

f <- make_functor(MyClass$new())
f(10)
#> [1] 11
f$x
#> [1] 1

f$x <- 2
f(10)
#> [1] 12
f$x
#> [1] 2
ifellows commented 5 years ago

Thanks for the reply. Good idea on the functor implementation.

I'd start by saying that this is "not a big deal," but let me make the case anyway.

  1. You agree that it is desirable for some R6 use cases to overload the indexing operator.
  2. There is no current way to overload the operator within the class system. The user is required to leave the class system and define an S3 method. This is what I would call "weird." Both S3 and S4 allow for [ and [[ overloading without leaving the class system.
  3. Since accessing an R6 object via x[ and x[<- will throw an error currently, there isn't much if any risk to supporting the operator overload. The only behavioral change would be to a class that implemented the x$[ method, and there shouldn't be any of those.
  4. Operator overloading is a very common feature object systems in languages other than R. I think that it is worth thinking carefully about how this could be supported in R6, especially if it is moving towards being a dominant object system in R. This would include thinking about %% operators.
  5. It just makes sense from a usability standpoint. If I want to implement [, I should create an [ R6 method.
hadley commented 5 years ago

I would strongly argue against providing a [ method (unless that error just throws an error message). The [ method needs to be compatible with length() and names(), and also [[ and $. Defining a [ method that is not compatible is going to cause many weird problems.

Another way of saying this is that R6 provides scalar objects. If you want to provide vector objects, you need to use a different OO system.

wch commented 5 years ago

@hadley Why does [ need to be compatible with length(), names(), [[, and $? Is there code in base R that uses [ with these other functions?

hadley commented 5 years ago

At least str() does.

The four methods are inextricably linked because length() tells you the valid set of integer values for use with [[, and names() tells you the valid set of character values. x[["y"]] should be equivalent to x$y, and has undefined behaviour if y is not in names(x)

hadley commented 5 years ago

FWIW base R doesn't ensure that [[ is compatible with length() but I think that is a continuing source of confusion, not a reason to support it in R6.

x <- new.env()
x$a <- 1
x$b <- 2

length(x)
#> [1] 2
x[[1]]
#> Error in x[[1]]: wrong arguments for subsetting an environment

Created on 2018-10-09 by the reprex package (v0.2.1)

hadley commented 4 years ago

It might now be possible to do this in a principled way, using what we've learned from writing vctrs.

sebffischer commented 2 years ago

Is there any activity on this feature?

hadley commented 2 years ago

@sebffischer if there was it, you'd see it in this issue.