Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I add additional arguments to methods for internal generics?

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
like image 469
maxheld Avatar asked Aug 16 '18 08:08

maxheld


2 Answers

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.

like image 110
hadley Avatar answered Nov 11 '22 17:11

hadley


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

like image 3
Moody_Mudskipper Avatar answered Nov 11 '22 16:11

Moody_Mudskipper