Skip to content

Custom group generics (and maybe simpler group generics) #506

@mjskay

Description

@mjskay

While I was waiting on #353 and #365, I started playing with an alternative approach to group generics in a package I am working on. I think this approach may be simpler than the current proposal, and also offer several benefits.

The basic idea is to add a general mechanism by which people can define methods on sets of generics at once. This could be implemented within method<-, or using a separate function.

The simple (probably not robust) implementation I am currently using looks like this:

#' Define methods on a group of generics
#'
#' Takes a list of generic functions and defines the same function for each,
#' allowing use of `.Generic` in the function body to refer to the actual generic.
#' @param generics a list of [symbol]s (such as returned by [expression()]),
#' each of which should correspond to a generic function, or the S4 definition
#' of a group generic, such as [`Compare`].
#' @param signature A method signature (see [`method<-`])
#' @param value A function implementing each `generic` for the given `signature`.
#' In each method, the variable `.Generic` in the method's environment will
#' refer to the generic function.
#' @noRd
`group_method<-` = \(generics, signature, value) {
  .generics = if (is(generics, "groupGenericFunction")) {
    lapply(getGroupMembers(generics), as.symbol)
  } else {
    generics
  }

  for (generic in .generics) {
    f = value
    environment(f) = new.env(parent = environment(value))
    environment(f)$.Generic = get(generic, mode = "function")
    eval(bquote(
      method(.(generic), signature) <<- f
    ))
  }
  invisible(generics)
}

Then you can use any existing S4 group generic as follows (say on a toy numeric class, "MyNum"):

group_method(Compare, list(MyNum, MyNum)) = \(e1, e2) MyNum(value = .Generic(e1@value, e2@value))

Since this generates all the specific methods (instead of creating a parallel dispatch on group generics), I think it makes dispatch easier to understand and easier to extend when creating class hierarchies.

It also avoids some of the ugliness common to S3 / S4 group generics code, like switch statements singling out one or two methods that operate differently. Instead, you would just define the specific methods you want to create as a separate group. e.g. if I just wanted to override inequalities, I might do:

inequalities = expression(`<`, `<=`, `>`, `>=`)    # or probably new_group_generic(...) or something
group_method(inequalities, list(MyNum, MyNum)) = \(e1, e2) MyNum(value = .Generic(e1@value, e2@value))

This also avoids the need for a method to look up the actual generic function inside the implementation, and allows users to create their own groups of generics as needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions