Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass local variables to methods in R >= 4.4.0

From ?UseMethod (also in the NEWS file):

UseMethod creates a new function call with arguments matched as they came in to the generic. [Previously local variables defined before the call to UseMethod were retained; as of R 4.4.0 this is no longer the case.]

With this dummy generic and its methods:

f <- function(x) {
  y <- head(x, 1)
  z <- tail(x, 1)
  UseMethod("f")
}

f.numeric <- function(x) {
  x + y + z
}

f.character <- function(x) {
  paste(x, y, z)
}

In R >= 4.4.0, we have:

f(1:3)
# Error in f.numeric(1:3) : object 'y' not found

f(c("a", "b", "c"))
# Error in f.character(c("a", "b", "c")) : object 'y' not found

While in R < 4.4.0, we had:

f(1:3)
# [1] 5 6 7

f(c("a", "b", "c"))
# [1] "a a c" "b a c" "c a c"

What is now the recommended way to pass local variables to methods?

My use case involves several objects that are precomputed from x before the call to UseMethod(). I am wondering if there is an alternative to wrapping this step in a function that would be called in each method, e.g.:

f <- function(x) {
  UseMethod("f")
}

g <- function(x) {
  y <- head(x, 1)
  z <- tail(x, 1)
  list(y = y, z = z)
}

f.numeric <- function(x) {
  yz <- g(x)
  x + yz$y + yz$z
}

f.character <- function(x) {
  yz <- g(x)
  paste(x, yz$y, yz$z)
}

This remain less practical than the former behavior because objects are now nested in a list. Anyway, I wish to avoid code duplication in the methods because the common step is relatively long (in LOC not time).

like image 471
Thomas Avatar asked Nov 23 '25 13:11

Thomas


2 Answers

How about this?

f <- function(x) {
  y <- head(x, 1)
  z <- tail(x, 1)
  f_int(x, y, z)
}

f_int <- function(x, ...)   UseMethod("f_int")

f_int.numeric <- function(x, y, z, ...) {
  x + y + z
}

f_int.character <- function(x, y, z, ...) {
  paste(x, y, z)
}

f(1:3)
#[1] 5 6 7

f(c("a", "b", "c"))
#[1] "a a c" "b a c" "c a c"
like image 195
Roland Avatar answered Nov 25 '25 03:11

Roland


What you appear to want is that f() and f.numeric() share an environment. So why not make that explicit, by defining both of them in an enclosing environment? Here's one way to do that:

# Create a local environment to be the common environment of the two 
# functions

fns <- local({
  # Create a dummy variable to hold the common local variable
  y <- NULL 

  f <- function(x) {
    # Modify the common variable
    y <<- 10
 
    # Dispatch to the method
    UseMethod("f")
  }

  f.numeric <- function(x) {
    # This function can see both the argument x and y in the enclosing
    # environment
    x + y
  }

  # Base R doesn't have "multi-assign", so we put these functions in a list
  list(f = f, f.numeric = f.numeric)
})

# Extract the two functions we just created.
f <- fns$f
f.numeric <- fns$f.numeric

f(1)
#> [1] 11

Created on 2024-06-03 with reprex v2.1.0

There are some issues with this approach. Perhaps you don't want those two definitions in the same file. Then you could change it to this:

f <- local({
  y <- NULL 

  function(x) {
    y <<- 10
    UseMethod("f")
  }
})

f.numeric <- function(x) {
  x + y
}
environment(f.numeric) <- environment(f)

f(1)
#> [1] 11

Created on 2024-06-03 with reprex v2.1.0

Now you only need to be sure that the definitions of both f and f.numeric happen before setting the environment. That can be done by forcing the collation sequence of your files, or putting the environment change into a zzz.R file.

I'd recommend against the second approach. Since both f() and f.numeric() are working on some common variables, it makes sense to me that they should be defined in the same source file. It would be really easy to change the local variables in f() without updating f.numeric() if they weren't.

like image 25
user2554330 Avatar answered Nov 25 '25 03:11

user2554330