Перегрузка оператора S3 для нескольких классов

Я определил два класса, которые могут успешно добавлять два своих объекта или число и один свой объект.

a <- structure(list(val = 1), class = 'customClass1')
b <- structure(list(val = 1), class = 'customClass2')
`+.customClass1` <- function(e1, e2, ...){
  val1 <- ifelse(is.numeric(e1), e1, e1$val)
  val2 <- ifelse(is.numeric(e2), e2, e2$val)
  val_res <- val1  + val2
  print('customClass1')
  return(structure(list(val = val_res), class = 'customClass1'))
}
`+.customClass2` <- function(e1, e2, ...){
  val1 <- ifelse(is.numeric(e1), e1, e1$val)
  val2 <- ifelse(is.numeric(e2), e2, e2$val)
  val_res <- val1  + val2
  print('customClass2')
  return(structure(list(val = val_res), class = 'customClass1'))
}
print.customClass1 <- function(x, ...){
  print(x$val)
}
print.customClass2 <- function(x, ...){
  print(x$val)
}
a + a
# [1] 2
a + 1
# [1] 2
b + b
# [1] 2
1 + b
# [1] 2

Но очевидно, что что-то пойдет не так, когда я попытаюсь добавить два пользовательских класса.

a + b
# Error in a + b : non-numeric argument to binary operator
# In addition: Warning message:
# Incompatible methods ("+.customClass1", "+.customClass2") for "+" 

Я мог бы определить только одну функцию для customClass1, но тогда эта функция не работала бы, когда я пытался добавить два объекта customClass2. Есть ли способ установить приоритет одной функции над другой?

Кажется, что R делает это естественным образом, отдавая приоритет моим функциям над базовыми функциями (например, числового или целочисленного типа). Когда один из обоих аргументов имеет тип customClass, R автоматически перенаправляет его в мою функцию вместо функции по умолчанию.


person takje    schedule 28.03.2017    source источник
comment
@JoshuaUlrich Я пытался понять, как R внутри выбирает одну функцию для определенного класса, а не другую. Когда два типа похожи, проблем не возникает. Когда один из обоих аргументов имеет базовый тип, а другой — нет, все работает нормально. Однако, когда вы делаете это с разными небазовыми типами, это сложнее. После переосмысления этого вопроса, вероятно, будет проще создать суперкласс, от которого оба customClass1 и 2 наследуют свойства.   -  person takje    schedule 29.03.2017
comment
@takje Это действительно было бы лучшим решением. Смотрите мой ответ, чтобы узнать, как это сделать.   -  person Joris Meys    schedule 29.03.2017


Ответы (2)


Как R выбирает метод для отправки, обсуждается в разделе Подробности документа ?base::Ops.

Классы обоих аргументов учитываются при отправке любого члена этой группы. Для каждого аргумента проверяется его вектор классов, чтобы увидеть, существует ли соответствующий конкретный (предпочтительный) метод или метод «Операции». Если метод найден только для одного аргумента или один и тот же метод найден для обоих, он используется. Если найдены разные методы, появляется предупреждение о «несовместимых методах»: в этом случае или если метод не найден ни для одного из аргументов, используется внутренний метод.

Если customClass1 и customClass2 связаны, вы можете использовать виртуальный класс, чтобы разрешить операции с использованием двух разных классов. Например, вы можете смешать POSIXct и POSIXlt, потому что они оба наследуются от POSIXt. Это задокументировано в ?DateTimeClasses:

"POSIXct" удобнее включать в фреймы данных, а "POSIXlt" ближе к удобочитаемым формам. Существует виртуальный класс "POSIXt", от которого наследуются оба класса: он используется, чтобы позволить таким операциям, как вычитание, смешивать два

Например:

class(pct <- Sys.time())
# [1] "POSIXct" "POSIXt"
Sys.sleep(1)
class(plt <- as.POSIXlt(Sys.time()))
# [1] "POSIXlt" "POSIXt"
plt - pct
# Time difference of 1.001677 secs

Если классы не связаны таким образом, есть полезная информация в ответах на Эмуляция множественной отправки с использованием S3 для метода "+" - возможно?.

person Joshua Ulrich    schedule 29.03.2017

Джошуа объяснил, почему ваш подход никогда не сможет работать гладко при использовании S3 без создания виртуальных суперклассов и тому подобного. С S3 вам придется вручную управлять назначениями классов во всех возможных функциях, которые вы используете. Забудьте назначить суперкласс один раз, и вы отправитесь на охоту за ошибками, которая может длиться некоторое время.

Я настоятельно рекомендую отказаться от S3 и перейти на S4. Затем вы можете определить методы в обоих направлениях для группы «Операции». Это имеет то преимущество, что все арифметические, логические операторы и операторы сравнения теперь определены для обоих классов. Если вы хотите ограничить это подгруппой или одним оператором, замените «Ops» подгруппой или оператором. Дополнительная информация на странице справки ?S4GroupGeneric.

Пример, основанный на ваших классах S3, использующих виртуальный класс, чтобы упростить задачу:

# Define the superclass
setClass("super", representation(x = "numeric"))
# Define two custom classes
setClass("foo", representation(slot1 = "character"),
         contains = "super")
setClass("bar", representation(slot1 = "logical"),
         contains = "super")

# Set the methods
setMethod("Ops",
          signature = c('super','ANY'),
          function(e1,e2){
            callGeneric(e1@x, e2)
          })
setMethod("Ops",
          signature = c('ANY','super'),
          function(e1,e2){
            callGeneric(e1, e2@x)
          })
# Redundant actually, but limits the amount of times callGeneric
# has to be executed. 
setMethod("Ops",
          signature = c('super','super'),
          function(e1,e2){
            callGeneric(e1@x, e2@x)
          })

foo1 <- new("foo", x = 3, slot1 = "3")
bar1 <- new("bar", x = 5, slot1 = TRUE)

foo1 + bar1
#> [1] 8
bar1 + foo1
#> [1] 8
bar1 < foo1
#> [1] FALSE
foo1 / bar1
#> [1] 0.6

Пример с двумя классами, где имена слотов разные:

setClass("foo", representation(x = "numeric"))
setClass("bar", representation(val = "numeric"))

setMethod("Ops",
          signature = c('foo','ANY'),
          function(e1,e2){
            callGeneric(e1@x, e2)
          })
setMethod("Ops",
          signature = c('bar','ANY'),
          function(e1,e2){
            callGeneric(e1@val, e2)
          })
setMethod("Ops",
          signature = c('ANY','bar'),
          function(e1,e2){
            callGeneric(e1, e2@val)
          })
setMethod("Ops",
          signature = c('ANY','foo'),
          function(e1,e2){
            callGeneric(e1, e2@x)
          })

Опять же, вы можете использовать приведенный выше код для проверки результатов. Обратите внимание, что здесь вы получите примечание о выбранных методах при интерактивной попытке. Чтобы этого избежать, вы можете добавить метод для подписи c('foo','bar') и c('bar','foo')

person Joris Meys    schedule 29.03.2017
comment
Это действительно хорошее предложение. До сих пор я использовал только S3. Я посмотрю на это, и это будет более чистое решение. Однако ответ Джошуа ближе к вопросу, и поэтому я принял его ответ. - person takje; 29.03.2017
comment
@takje не беспокойтесь, я просто добавил этот ответ, чтобы проиллюстрировать, как S4 решает эту проблему более надежным образом. Если хотите попробовать: у Хэдли есть отличное введение в S4 в его книге Advanced R: adv-r.had.co.nz/S4.html В своей книге о пакетах он также объясняет, как можно легко экспортировать и документировать классы и методы S4 с помощью roxygen2: r-pkgs.had.co.nz/man.html#man-classes и r-pkgs.had.co.nz/namespace.html#exports< /а> - person Joris Meys; 29.03.2017