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

Я хотел бы написать метод %*% для подкласса базовой матрицы. Мой подкласс является классом S3, и в документации help("%*%") говорится, что %*% является универсальным S4 и что методы S4 необходимо написать для функции с двумя аргументами, названными x и y. Я написал методы для классов S3 для универсальных методов S4 перед использованием methods::setOldClass() и methods::setMethod(), и я просмотрел исходный код пакета {Matrix} для вдохновения, но по какой-то причине я не могу заставить это работать в этом случае. 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" 

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

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

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

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"
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
9
0
480
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

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;
}

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

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

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

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

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

  • %*% не является общим для S3, поэтому даже если вы зарегистрируете метод S3, такой как %*%.zzz, он не будет отправлен.

При этом оператор %*% станет универсальным для 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.

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