11

I want to implement an inset method for my class myClass for the internal generic [<- (~ help(Extract)). This method should run a bunch of tests, before passing on the actual insetting off to [<- via NextMethod().

I understand that:

  • any method has to include at least the arguments of the generic (mine does, I think)
  • the NextMethod() call does not usually need any arguments (though supplying them manually doesn't seem to help either).

Here's my reprex:

x <- c(1,2)
class(x) <- c("myClass", "numeric")

`[<-.myClass` <- function(x, i, j, value, foo = TRUE, ...) {
  if (foo) {
    stop("'foo' must be false!")
  }
  NextMethod()
}

x[1] <- 3  # this errors out with *expected* error message, so dispatch works
x[1, foo = FALSE] <- 3  # this fails with "incorrect number of subscripts

What seems to be happening is that NextMethod() also passes on foo to the internal generic [<-, which mistakes foo for another index, and, consequently errors out (because, in this case, x has no second dimension to index on).

I also tried supplying the arguments explicitly no NextMethod(), but this also fails (see reprex below the break).

How can I avoid choking up NextMethod() with additional arguments to my method?

(Bonus: Does anyone know good resources for building methods for internal generics? @Hadleys adv-r is a bit short on the matter).


Reprex with explicit arguments:

x <- c(1,2)
class(x) <- c("myClass", "numeric")

`[<-.myClass` <- function(x, i = NULL, j = NULL, value, foo = TRUE, ...) {
  if (foo) {
    stop("'foo' must be false!")
  }
  NextMethod(generic = "`[<-`", object = x, i = i, j = j, value = value, ...)
}

x[1] <- 3  # this errors out with expected error message, so dispatch works
x[1, foo = FALSE] <- 3  # this fails with "incorrect number of subscripts
maxheld
  • 3,963
  • 2
  • 32
  • 51
  • 1
    can't you copy some code of `data.table`? I think that's what they achieved by adding the `,by` statement? – Andre Elrico Aug 16 '18 at 09:25
  • sorry, I'm not familiar with (the internals of) the *data.table* package. Does it have a ``[<-.data.table`` method? Can you point me to some file in the source where I can find it? – maxheld Aug 16 '18 at 09:31
  • 2
    `data.table` doesn't use `NextMethod` : run this to see the code: `data.table:::'[<-.data.table'` – moodymudskipper Aug 16 '18 at 09:35
  • search and get inspiration here: `https://github.com/Rdatatable/data.table` – Andre Elrico Aug 16 '18 at 10:17
  • 1
    please, keep us up to date with your progress! – Andre Elrico Aug 16 '18 at 10:20
  • @maxheld do you really need `NextMethod` ? What are you actually trying to do ? – moodymudskipper Aug 16 '18 at 10:49
  • thanks @AndreElrico and @Moody_Mudskipper for the suggestions; I *was* in fact looking for a way with `NextMethod()`, because a) it's elegant/idiomatic and b) I just wanted to better understand how dispatch works for internal generics. The `data.table:::'[<-.data.table'` solution doesn't use `NextMethod()` as @Moody_Mudskipper pointed out. *Without* `NextMethod()` I'd just remove class `myClass` from `x`, and then use the generic `[<-`. That would be the easiest workaround in my case. But, as I said, I wanted to understand `NextMethod()` and don't see why it shouldn't work here. – maxheld Aug 16 '18 at 11:54
  • @Moody_Mudskipper what I'm actually trying to accomplish is just to run a bunch of assertions before allowing `[<-` for `myClass`. So I can get this done simply as in the above. I don't really need `NextMethod()` just want to understand why it won't work here. – maxheld Aug 16 '18 at 11:56

2 Answers2

5

I don't see an easy way around this except to strip the class (which makes a copy of x)

`[<-.myClass` <- function(x, i, value, ..., foo = TRUE) {
  if (foo) {
    cat("hi!")
    x
  } else {
    class_x <- class(x)
    x <- unclass(x)
    x[i] <- value
    class(x) <- class_x
    x
  }
}

x <- structure(1:2, class = "myClass")
x[1] <- 3
#> hi!

x[1, foo = FALSE] <- 3
x
#> [1] 3 2
#> attr(,"class")
#> [1] "myClass"

This is not a general approach - it's only needed for [, [<-, etc because they don't use the regular rules for argument matching:

Note that these operations do not match their index arguments in the standard way: argument names are ignored and positional matching only is used. So m[j = 2, i = 1] is equivalent to m[2, 1] and not to m[1, 2].

(from the "Argument matching" section in ?`[`)

That means your x[1, foo = FALSE] is equivalent to x[1, FALSE] and then you get an error message because x is not a matrix.

Approaches that don't work:

  • Supplying additional arguments to NextMethod(): this can only increase the number of arguments, not decrease it

  • Unbinding foo with rm(foo): this leads to an error about undefined foo.

  • Replacing foo with a missing symbol: this leads to an error that foo is not supplied with no default argument.

hadley
  • 102,019
  • 32
  • 183
  • 245
3

Here's how I understand it, but I don't know so much about that subject so I hope I don't say too many wrong things.

From ?NextMethod

NextMethod invokes the next method (determined by the class vector, either of the object supplied to the generic, or of the first argument to the function containing NextMethod if a method was invoked directly).

Your class vector is :

x <- c(1,2)
class(x) <- "myClass" # note: you might want class(x) <- c("myClass", class(x))
class(x) # [1] "myClass"

So you have no "next method" here, and [<-.default, doesn't exist.

What would happen if we define it ?

`[<-.default` <- function(x, i, j, value, ...) {print("default"); value} 

x[1, foo = FALSE] <- 3 
# [1] "default"
x
# [1] 3

If there was a default method with a ... argument it would work fine as the foo argument would go there, but it's not the case so I believe NextMethod just cannot be called as is.

You could do the following to hack around the fact that whatever is called doesn't like to be fed a foo argument:

`[<-.myClass` <- function(x, i, j, value, foo = FALSE, ...) {
  if (foo) {
    stop("'foo' must be false!")
  }

  `[<-.myClass` <- function(x, i, j, value, ...) NextMethod()
  args <- as.list(match.call())[-1]
  args <- args[names(args) %in% c("","x","i","j","value")]
  do.call("[<-",args)
}

x[1, foo = FALSE] <- 3
x
# [1] 3 2
# attr(,"class")
# [1] "myClass"

Another example, with a more complex class :

library(data.table)
x        <- as.data.table(iris[1:2,1:2])
class(x) <- c("myClass",class(x))

x[1, 2, foo = FALSE] <- 9999
#    Sepal.Length Sepal.Width
# 1:          5.1        9999
# 2:          4.9           3

class(x)
# [1] "myClass"    "data.table" "data.frame"

This would fail if the next method had other arguments than x, i, j and value, in that case better to be explicit about our additional arguments and run args <- args[! names(args) %in% c("foo","bar")]. Then it might work (as long as arguments are given explicitly as match.call doesn't catch default arguments). I couldn't test this though as I don't know such method for [<-.

moodymudskipper
  • 46,417
  • 11
  • 121
  • 167
  • "*So you have no "next method" here, and `[<-.default`, doesn't exist.*" I don't think *that* is the problem here, but I have updated my above reprex accordingly. The reason why dispatch on base types works even without class attributes is, I think, part of the design of inherit generics. [Writes](https://adv-r.hadley.nz/s3.html#s3-and-base-types) @hadley: "*internal generics do not dispatch to methods unless the class attribute has been set (is.object() is true). This means that internal generics do not use the implicit class.*" So I don't think the "missing" base class is the problem. – maxheld Aug 16 '18 at 12:05
  • and thanks for `NextMethod()` workaround @Moody_Mudskipper: I'll do something like that. Still leaving this open until we figure out the reason why `NextMethod()` won't work. – maxheld Aug 16 '18 at 12:06
  • 1
    I updated with an alternate workaround, that doesn't unclass, so a bit cleaner, and it uses `NextMethod` – moodymudskipper Aug 16 '18 at 13:31
  • Since this is a vector, not a matrix, you can drop the `j` argument and avoid the `do.call()` gymnastics in favour `x[i] <- value`. But I don't know if I'd describe redefining the method inside itself as cleaner – hadley Aug 16 '18 at 13:34
  • It's to keep it general in case OP wants to use the j argument, I said cleaner because it's more general, unclass seemed a bit "brutal", in a general case the object could also have the class data.frame for instance. I agree for OP's precise case. – moodymudskipper Aug 16 '18 at 13:37
  • 1
    I'm not sure that's good practice for `[` since the arguments have such unusual properties (i.e. no matching based on names). We don't have quite enough information here to know if `myClass` is subclassable, but generally I think you should presume it's not, since making subclasses work correctly requires considerable extra thought at all layers on the class implementation. – hadley Aug 16 '18 at 13:43