Как я могу написать метод `%*%` для подкласса базовой матрицы S3?

Я хотел бы написать метод для подкласса базовой матрицы. Мой подкласс - это класс S3 и документацияhelp("%*%")говорит, что это универсальный вариант S4 и что методы S4 необходимо написать для функции двух аргументов с именемxиy. Я написал методы для классов S3 для универсальных методов S4 перед использованием.methods::setOldClass()иmethods::setMethod()и я просмотрел исходный код для{Matrix}package для вдохновения, но по какой-то причине я не могу заставить его работать в данном случае.methods::showMethods()показывает, что мой метод существует для моей целевой сигнатуры, но, похоже, мой метод никогда не вызывается R, когда я пытаюсь его использовать.

      x <- diag(3)
class(x) <- c("atm2d", class(matrix()))
print(x)
           [,1] [,2] [,3]
[1,]    1    0    0
[2,]    0    1    0
[3,]    0    0    1
attr(,"class")
[1] "atm2d"  "matrix" "array" 

По умолчанию атрибут моего класса удаляется, и я хотел бы его сохранить.



Я попытался создать метод для своего класса, который не избавился бы от атрибута моего класса:

      as.matrix.atm2d <- function(x, ...) {
    class(x) <- NULL
    x
}
matmult <- function(x, y) {
    v <- as.matrix(x) %*% as.matrix(y)
    class(v) <- c("atm2d", class(matrix()))
    v
}
methods::setOldClass("atm2d")
# this alternate `setOldClass()` also doesn't work
# methods::setOldClass(c("atm2d", class(matrix()))) 
methods::setMethod("%*%", 
                   c(x = "atm2d", y = "atm2d"), 
                   function(x, y) matmult(x, y))

showMethods()кажется, что был создан метод S4 с ожидаемой сигнатурой:

      showMethods("%*%", class = "atm2d")
      Function: %*% (package base)
x="atm2d", y="atm2d"

Однако этот метод на самом деле не вызывается%*%:

      print(x %*% x)
           [,1] [,2] [,3]
[1,]    1    0    0
[2,]    0    1    0
[3,]    0    0    1

Если бы мой метод был вызван, я бы ожидал, что он также распечатает свой класс:

      print(matmult(x, x))
           [,1] [,2] [,3]
[1,]    1    0    0
[2,]    0    1    0
[3,]    0    0    1
attr(,"class")
[1] "atm2d"  "matrix" "array"

1 ответ

Оператор является внутренне универсальным, что означает, что отправка происходит в коде C. В настоящее время (т.е. в R 4.2.3) соответствующая функция C (определенная здесь) содержит такую ​​проверку:

      if (PRIMVAL(op) == 0 && /* %*% is primitive, the others are .Internal() */
   (IS_S4_OBJECT(x) || IS_S4_OBJECT(y))
   && R_has_methods(op)) {
    SEXP s, value;
    /* Remove argument names to ensure positional matching */
    for(s = args; s != R_NilValue; s = CDR(s)) SET_TAG(s, R_NilValue);
    value = R_possible_dispatch(call, op, args, rho, FALSE);
    if (value) return value;
}

Если ни один из них не является объектом S4, как в вашем примере, тоdo_matprodпродолжает обращаться с ними как с традиционными матрицами, не обращая внимания наclassатрибут любого аргумента. Разделhelp("%*%")на что вы ссылаетесь:

Этот оператор является универсальным для S4, но не универсальным для S3. Методы S4 необходимо писать для функции двух аргументов с именемxиy.

пытается это выразить, но это не особенно ясно. (В конце концов, вы определили метод S4.)

Здесь есть две основные проблемы:

  • позволяет определять методы S4 с классами S3 в сигнатуре, но внутренние универсальные функции ищут методы S4 только тогда, когда одним из аргументов является объект S4 (для скорости).

  • не является универсальным для S3, поэтому даже если вы зарегистрировали метод S3, например , он не будет отправлен.

При этом оператор станет универсальным для S3, начиная с версии R 4.3.0, которая выйдет 21 апреля. Эту запись вы найдете в НОВОСТЯХ:

Оператор умножения матриц теперь является универсальным оператором S3 и принадлежит новой группе дженериков.matrixOps. Из статьи Томаша Калиновского в PR#18483.

Когда это произойдет, он будет вести себя так же, как и другие внутренне общие членыOpsгруппа, в которой методы S3%*%.zzzбудут отправлены туда, где это необходимо. Однако методы S4 по-прежнему не будут отправляться, если ни один из аргументов не является объектом S4.

Возможно,setMethodследует изменить, чтобы сигнализировать о предупреждении или ошибке при попытке определить методы S4, которые никогда не будут отправлены, как в вашем примере.


Приложение

Возможно, было бы полезно перечислить типы универсальных функций и типы методов, которые они отправляют, ограничив внимание S3 и S4. Мы будем использовать этот скрипт для определения объектов для наших тестов, каждый из которых должен запускаться в новом процессе R:

      ## objects.R
w <- structure(0, class = "a")
x <- structure(0, class = "b")

setOldClass("c")
y <- structure(0, class = "c")

setClass("d", contains = "numeric")
z <- new("d", 0)

Не внутренние общие функции S3

Эти методы S3 отправляются для классов S3 и классов S4 черезUseMethod. Если метод не найден, они отправляют метод по умолчанию.*.defaultили (если он не найден) выдать ошибку. Они никогда не отправляют методы S4.

      source("objects.R")

h <- .__h__. <- function(x) UseMethod("h")
.S3method("h", "default", function(x) "default")

.S3method("h", "a", function(x) "a3")
setMethod("h", "c", function(x) "c4")
setMethod("h", "d", function(x) "d4")

h <- .__h__. # needed to undo side effect of 'setMethod'
h
## function(x) UseMethod("h")

h(w)
## [1] "a3"
h(x)
## [1] "default"
h(y)
## [1] "default"
h(z)
## [1] "default"

Не внутренние общие функции S4

Эти методы отправки S4 для классов S3(формально определенных с помощьюsetOldClass) и классы S4 черезstandardGeneric. Если метод не найден, они отправляют метод по умолчанию.*@default. Если метод по умолчанию является универсальным методом S3, то отправка происходит снова, на этот раз для всех доступных методов S3. Однако часто метод по умолчанию просто вызываетstopчтобы сигнализировать об ошибке.

      source("objects.R")

h <- function(x) UseMethod("h")
.S3method("h", "default", function(x) "default")

.S3method("h", "c", function(x) "c3")
setMethod("h", "d", function(x) "d4")

h
## standardGeneric for "h" defined from package ".GlobalEnv"
## 
## function (x) 
## standardGeneric("h")
## <environment: 0x1044650b0>
## Methods may be defined for arguments: x
## Use  showMethods(h)  for currently available ones.

h@default
## Method Definition (Class "derivedDefaultMethod"):
## 
## function (x) 
## UseMethod("h")
## 
## Signatures:
##         x    
## target  "ANY"
## defined "ANY"

h(w)
## [1] "default"
h(x)
## [1] "default"
h(y)
## [1] "c3"
h(z)
## [1] "d4"

Внутренне универсальные функции

Все они определены в базе как примитивные функции. Вам следует обратиться к страницам справки или к исходному коду, чтобы определить, являются ли они только универсальными для S3, только универсальными для S4 или универсальными для S3 и S4. В третьем случае отправка S3 происходит только в том случае, если не найдены подходящие методы S4. И, как я уже объяснял, отправка S4 происходит только в том случае, если один из аргументов в сигнатуре является объектом S4.

Давайте возьмем и%*%в качестве примеров. Оба являются общими для S4, но только+является универсальным для S3.

      source("objects.R")

.S3method("+", "default", function(e1, e2) "default")

.S3method("+", "a", function(e1, e2) "a3")
.S3method("+", "b", function(e1, e2) "b3")
.S3method("+", "c", function(e1, e2) "c3")
.S3method("+", "d", function(e1, e2) "d3")

setMethod("+", c("c", "c"), function(e1, e2) "c4")
setMethod("+", c("d", "d"), function(e1, e2) "d4")

w + w
## [1] "a3"
x + x
## [1] "b3"
y + y
## [1] "c3"
z + z
## [1] "d4"

Здесь отправляются методы S3. Первые три результата получаются посредством диспетчеризации S3. Последний результат получается посредством диспетчеризации S4.

      source("objects.R")

.S3method("%*%", "default", function(x, y) "default")

.S3method("%*%", "a", function(x, y) "a3")
.S3method("%*%", "b", function(x, y) "b3")
.S3method("%*%", "c", function(x, y) "c3")
.S3method("%*%", "d", function(x, y) "d3")

setMethod("%*%", c("c", "c"), function(x, y) "c4")
setMethod("%*%", c("d", "d"), function(x, y) "d4")

w %*% w
##      [,1]
## [1,]    0
x %*% x
##      [,1]
## [1,]    0
y %*% y
##      [,1]
## [1,]    0
z %*% z
## [1] "d4"

Здесь методы S3 не отправляются. Первые три результата получаются с помощью внутреннего метода по умолчанию. Последний результат получается посредством диспетчеризации S4.

Другие вопросы по тегам