Изменить объект S3, не возвращая его?

Я новичок в объектно-ориентированном программировании на R и борюсь с тем, как правильно написать функцию, которая модифицирует объект.

Этот пример работает:

store1 <- list(
  apples=3,
  pears=4,
  fruits=7
)
class(store1) <- "fruitstore"
print.fruitstore <- function(x) {
  paste(x$apples, "apples and", x$pears, "pears", sep=" ")
}
print(store1)
addApples <- function(x, i) {
x$apples <- x$apples + i
x$fruits <- x$apples + x$pears
return(x)
}
store1 <- addApples(store1, 5)
print(store1)

Но я предполагаю, что должен быть более чистый способ сделать это, не возвращая весь объект:

addApples(store1, 5)  # Preferable line...
store1 <- addApples(store1, 5)  # ...instead of this line

Как правильно писать модифицирующие функции в R? "<< -"?

Обновление: Спасибо всем за то, что стал Rosetta Stone для ООП в R. Очень информативно. Проблема, которую я пытаюсь решить, очень сложна с точки зрения последовательности действий, поэтому жесткость эталонных классов может помочь структуре. Я хотел бы принять все ответы в качестве ответов, а не только один.

4 ответа

Решение

Вот реализация эталонного класса, как предлагается в одном из комментариев. Основная идея заключается в создании ссылочного класса Stores это имеет три поля: apples, pears а также fruits (отредактировано, чтобы быть методом доступа). initialize метод используется для инициализации нового магазина, addApples метод добавляет яблоки в магазин, в то время как show метод эквивалентен print для других объектов.

Stores = setRefClass("Stores", 
  fields = list(
    apples = "numeric",
    pears  = "numeric",
    fruits = function(){apples + pears}
  ), 
  methods = list(
    initialize = function(apples, pears){
      apples <<- apples
      pears <<- pears
    },
    addApples = function(i){
      apples <<- apples + i
    },
    show = function(){
      cat(apples, "apples and", pears, "pears")
    }
  )
)

Если мы инициализируем новый магазин и называем его, вот что мы получаем

FruitStore = Stores$new(apples = 3, pears = 4)
FruitStore

# 3 apples and 4 pears

Теперь, ссылаясь на addApples метод, давайте добавим 4 яблока в магазин

FruitStore$addApples(4)
FruitStore

# 7 apples and 4 pears

РЕДАКТИРОВАТЬ. Согласно предложению Хэдли, я обновил свой ответ так, чтобы fruits теперь метод доступа. Это остается обновленным, поскольку мы добавляем больше apples в магазин. Спасибо, Хэдли.

На самом деле вы можете сделать это с классами S3 с функциями замены, если вы хотите сэкономить, погружаясь в справочные классы. Во-первых, ваш пример

store1 <- list(apples=3,pears=4)
class(store1) <- "fruitstore"
print.fruitstore <- function(x) {
  x <- paste(unlist(store1), names(store1), collapse=", ")
  x <- paste0(x, " for a total of ", sum(unlist(store1)), " fruit.")
  NextMethod()
}
store1
# [1] "3 apples, 4 pears for a total of 7 fruit."

Обратите внимание, как с помощью NextMethod значит я не должен делать print(store1)Я могу просто напечатать store, В основном, как только я переназначу x чтобы быть тем, что я хочу показать на экране, я просто отправляю по умолчанию print метод. Затем:

`addapples<-` <- function(x, ...) UseMethod("addapples<-")
`addapples<-.fruitstore` <- function(x, value) {
  x[["apples"]] <- x[["apples"]] + value
  x
}
addapples(store1) <- 4
store1
# [1] "7 apples, 4 pears for a total of 11 fruit."

Тада! Опять же, не совсем типичный случай использования S3. За исключением [ а также [[ функции, функции замены, как правило, предназначены для обновления атрибутов (например, класса, длины и т. д.), но я не вижу слишком большого вреда в его расширении.

Обратите внимание, что это не реальное назначение ссылки. На самом деле, ваш fruitstore Объект копируется, изменяется и повторно назначается исходной переменной (см. R Docs).

Вы должны взглянуть на пакет data.table.

Загрузите пакет: library(data.table)

Определите объект data.table:

store1 <- data.table(apples=3,
                 pears=4,
                 fruits=7)

Затем определите функцию addApple:

 addApple <- function(data,i) {data[,apples:=(data[1,apples]+i)]; 
                             data[,fruits:=(data[1,apples]+data[1,pears])]}

Вот и все.

Когда ты пишешь addApple(store1) Вы должны получить + я яблоки и фрукты "яблоки + груши".

И вы все равно можете определить свой класс S3, если хотите, просто убедитесь, что он наследует классы data.frame и data.table.

class(store1) <- c("fruitstore",class(store1))

Еще одна вещь, вы должны переписать свой метод S3 для печати, сделав print явным:

print.fruitstore <- function(x) {
   print(paste(x$apples, "apples and", x$pears, "pears", sep=" "))
}

Или вы можете просто использовать cat:

print.fruitstore <- function(x) {cat(x$apples, "apples and", x$pears, "pears")}

Вот реализация, использующая пакет proto, который объединяет объекты и классы в единую концепцию прототипов. Например, здесь действительно нет разницы между Fruitstore который является объектом, который играет роль класса и store1 который является объектом, который играет роль конкретного магазина. Они оба прото-объекты.

library(proto)

Fruitstore <- proto(
    addApples = function(., i) {
        .$apples <- .$apples + i
        .$fruits <- .$apples + .$pears
    },
    print = function(.) cat(.$apples, "apples and", .$pears, "pears\n")
)

# define store1 as a child of Fruitstore
store1 <- Fruitstore$proto(apples = 3, pears = 4, fruits = 7)

store1$addApples(5)
store1$print()
Другие вопросы по тегам