Skip to content

Complete S3 method registration#643

Merged
hadley merged 10 commits into
mainfrom
expand-s3-registration
May 29, 2026
Merged

Complete S3 method registration#643
hadley merged 10 commits into
mainfrom
expand-s3-registration

Conversation

@hadley
Copy link
Copy Markdown
Member

@hadley hadley commented May 25, 2026

Part of #455

@hadley hadley requested a review from t-kalinowski May 26, 2026 14:25
Comment thread R/method-register.R Outdated
Comment thread tests/testthat/test-method-register.R Outdated
it("S3 registration requires a S7 class", {
foo <- new_class("foo")
it("can register S7 method for S3 generic with base type signature", {
local_s3_generic("s3_gen")
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@t-kalinowski I think this looks better now — it uses the technique that I remember, i.e. you need to register the generic in the global namespace. This is a bit ugly but I don't think there's much that S7 can do about it.

@hadley hadley requested a review from t-kalinowski May 28, 2026 15:23
Copy link
Copy Markdown
Member

@t-kalinowski t-kalinowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunatly, the snippet I gave earlier still fails:

s3_gen <- local({
  function(x) UseMethod("s3_gen")
})

local({
  method(s3_gen, class_character) <- function(x) "char"
  method(s3_gen, class_integer) <- function(x) "int"
})

s3_gen("foo")
#> Error in UseMethod("s3_gen") :
#>   no applicable method for 's3_gen' applied to an object of class "character"

I think this is a limitation of registerS3method(). It registers into environment(generic)$.__S3MethodsTable__., but UseMethod() consults the table associated with topenv(environment(generic)). For normal top-level/package generics these are the same, but for local/generated generics they differ.

If we manually assign into the table that UseMethod() actually consults, it works:

s3_gen <- local({
  function(x) UseMethod("s3_gen")
})

table_env <- topenv(environment(s3_gen))
table_env$.__S3MethodsTable__. <- new.env(hash = TRUE, parent = baseenv())

assign(
  "s3_gen.character",
  function(x) "char",
  envir = table_env$.__S3MethodsTable__.
)

s3_gen("foo")
#> [1] "char"

So I think the patch should keep using base registerS3method() generally, but branch for this special case:

register_S3_method <- function(generic, signature, method, envir = parent.frame()) {
  class <- s3_signature_class(signature[[1]])

  generic_fun <- get(generic$name, mode = "function", envir = envir)
  generic_env <- environment(generic_fun)
  dispatch_env <- topenv(generic_env)

  if (!identical(generic_env, dispatch_env)) {
    table <- dispatch_env[[".__S3MethodsTable__."]]
    if (is.null(table)) {
      table <- new.env(hash = TRUE, parent = baseenv())
      dispatch_env[[".__S3MethodsTable__."]] <- table
    }

    assign(paste(generic$name, class, sep = "."), method, envir = table)
    return(invisible(method))
  }

  registerS3method(generic$name, class, method, envir = envir)
  invisible(method)
}

That lets the ordinary path continue to delegate to base R, while avoiding registerS3method() for the case that it does not handle ideally.

@hadley
Copy link
Copy Markdown
Member Author

hadley commented May 29, 2026

Can you expand on your examples using local() are a use case that we should support? I don't think the goal here is to support every weird and wacky way that you might try to register S3 methods, but instead make this work for the typical use case (i.e. defining S3 methods in an package).

That said, your proposed fix isn't a lot of code, so maybe it's worth patching around the lack of comprehensive API in base R? It just makes me feel a little uncomfortable to be providing tools for registering S3 methods that don't exist elsewhere.

@t-kalinowski
Copy link
Copy Markdown
Member

I'm thinking in general of the category of systems like https://github.com/klmr/box, https://github.com/rticulate/import, https://github.com/wahani/modules, (or my own https://t-kalinowski.github.io/envir/reference/import_from.html when used with an R script) where functions can be defined in one environment and then can be made available in another.

@hadley
Copy link
Copy Markdown
Member Author

hadley commented May 29, 2026

I'm a little sceptical that this is important but it's also not that much code.

@hadley hadley merged commit 6e4befa into main May 29, 2026
13 checks passed
@hadley hadley deleted the expand-s3-registration branch May 29, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants