Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Modify S3 object without returning it?

Tags:

r

r-s3

I'm new to object-oriented programming in R and struggle with how to properly write a function that modifies an object.

This example works:

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)

But I suppose there should be a cleaner way to do this without returning the whole object:

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

What is the proper way to write modify-functions in R? "<<-"?

Update: Thank you all for what became a Rosetta Stone for OOP in R. Very informative. The problem I'm trying to solve is very complex in terms of flow, so the rigidness of reference classes may bring the structure to help. I wish I could accept all responses as answers and not only one.

like image 605
Chris Avatar asked Dec 16 '22 02:12

Chris


2 Answers

You can actually do this with S3 classes with replacement functions if you want to save yourself diving into reference classes. First, your example

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

Notice how using NextMethod means I don't have to do print(store1), I can just type store. Basically, once I re-assign x to be what I want to show up on screen, I just dispatch the default print method. Then:

`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."

Tada! Again, not really the typical S3 usage case. Except for the [ and [[ functions, replacement functions are typically intended to update attributes (e.g. class, length, etc.), but I don't see too much harm in stretching that.

Note this is not a real by reference assignment. Really, your fruitstore object is copied, modified, and re-assigned to the original variable (see R Docs).

like image 80
BrodieG Avatar answered Dec 30 '22 21:12

BrodieG


Here is a reference class implementation, as suggested in one of the comments. The basic idea is to set up a reference class called Stores that has three fields: apples, pears and fruits (edited to be an accessor method). The initialize method is used to initialize a new store, the addApples method adds apples to the store, while the show method is equivalent to print for other objects.

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")
    }
  )
)

If we initialize a new store and call it, here is what we get

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

# 3 apples and 4 pears

Now, invoking the addApples method, let us add 4 apples to the store

FruitStore$addApples(4)
FruitStore

# 7 apples and 4 pears

EDIT. As per Hadley's suggestion, I have updated my answer so that fruits is now an accessor method. It remains updated as we add more apples to the store. Thanks @hadley.

like image 23
Ramnath Avatar answered Dec 30 '22 22:12

Ramnath