diff --git a/NAMESPACE b/NAMESPACE index f2e43419..21c1d925 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -8,6 +8,7 @@ S3method("[<-",S7_object) S3method("[[",S7_object) S3method("[[<-",S7_object) S3method("|",S7_class) +S3method(Math,S7_object) S3method(Ops,S7_object) S3method(Ops,S7_super) S3method(c,S7_class) @@ -37,6 +38,10 @@ export("method<-") export("prop<-") export("props<-") export(S4_register) +export(S7_Complex) +export(S7_Math) +export(S7_Ops) +export(S7_Summary) export(S7_class) export(S7_data) export(S7_dispatch) @@ -64,6 +69,7 @@ export(class_numeric) export(class_raw) export(class_vector) export(convert) +export(find_base_generic) export(method) export(method_explain) export(methods_register) diff --git a/R/method-group.R b/R/method-group.R new file mode 100644 index 00000000..496c74f3 --- /dev/null +++ b/R/method-group.R @@ -0,0 +1,85 @@ +#' S7 Group Generics +#' +#' Group generics allow you to implement methods for many generics at once. +#' You cannot call a group generic directly; instead it is called automatically +#' by members of the group if a more specific method is not found. For example, +#' if you define a method for the `S7_Math` group generic, it will be called +#' when you call `abs()`, `sign()`, `sqrt()`, and many other similar generics +#' (see below for a complete list). +#' +#' @param x,z,e1,e2 Objects used for dispatch. +#' @param ...,na.rm Additional arguments passed to methods. +#' @param .Generic The name of the generic being dispatched on, i.e. if you've +#' defined a method for `S7_Math` and the user calls `abs()` then `.Generic` +#' will be `"abs"`. +#' +#' Use `find_base_generic()` to find the base generic that corresponds to the +#' generic name. +#' @details +#' # Methods +#' +#' The group generics contain the following methods: +#' +#' * `Ops`: `r group_generics_md("Ops")` +#' * `Math`: `r group_generics_md("Math")` +#' * `Summary`: `r group_generics_md("Summary")` +#' * `Complex`: `r group_generics_md("Complex")` +#' * `matrixOps`: `r group_generics_md("matrixOps")` +#' +#' @name S7_group_generics +NULL + +#' @export +#' @rdname S7_group_generics +S7_Math <- NULL + +#' @export +#' @rdname S7_group_generics +S7_Ops <- NULL + +#' @export +#' @rdname S7_group_generics +S7_Complex <- NULL + +#' @export +#' @rdname S7_group_generics +S7_Summary <- NULL + +on_load_define_group_generics <- function() { + S7_Math <<- new_generic("Math", "x", function(x, ..., .Generic) { + S7_dispatch() + }) + + S7_Ops <<- new_generic("Ops", c("e1", "e2"), function(e1, e2, ..., .Generic) { + S7_dispatch() + }) + + S7_Complex <<- new_generic("Complex", "z", function(z, ..., .Generic) { + S7_dispatch() + }) + + S7_Summary <<- new_generic("Summary", "x", function(x, ..., na.rm = FALSE, .Generic) { + S7_dispatch() + }) +} + +#' @export +Math.S7_object <- function(x, ...) { + tryCatch( + return(S7_Math(x, ..., .Generic = .Generic)), + S7_error_method_not_found = function(cnd) NULL + ) + + NextMethod() +} + +#' @export +#' @rdname S7_group_generics +find_base_generic <- function(.Generic) { + get(.Generic, mode = "function", envir = baseenv()) +} + + +group_generics_md <- function(name) { + paste0("`", group_generics()[[name]], "`", collapse = ", ") +} diff --git a/R/method-ops.R b/R/method-ops.R index a39ee00b..39d5f1f1 100644 --- a/R/method-ops.R +++ b/R/method-ops.R @@ -16,17 +16,24 @@ on_load_define_ops <- function() { #' @export Ops.S7_object <- function(e1, e2) { + # Try "specific" generic cnd <- tryCatch( return(base_ops[[.Generic]](e1, e2)), S7_error_method_not_found = function(cnd) cnd ) - if (S7_inherits(e1) && S7_inherits(e2)) { - stop(cnd) - } else { - # Must call NextMethod() directly in the method, not wrapped in an - # anonymous function. + # Try group generic + cnd <- tryCatch( + return(S7_Ops(e1, e2, .Generic = .Generic)), + S7_error_method_not_found = function(cnd) cnd + ) + + if (!S7_inherits(e1) || !S7_inherits(e2)) { + # Fall back to base behaviour. Must call NextMethod() directly here, not + # wrapped in an anonymous function. NextMethod() + } else { + stop(cnd) } } diff --git a/R/zzz.R b/R/zzz.R index b4f463e5..1ab61f92 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -122,6 +122,7 @@ methods::setOldClass(c("S7_method", "function", "S7_object")) activate_backward_compatiblility() on_load_make_convert_generic() + on_load_define_group_generics() on_load_define_ops() on_load_define_or_methods() on_load_define_S7_type() diff --git a/_pkgdown.yml b/_pkgdown.yml index 9406eeef..e1ae0757 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -30,6 +30,7 @@ reference: - method_explain - super - S7_class + - S7_group_generics - title: Packages desc: > diff --git a/man/S7_group_generics.Rd b/man/S7_group_generics.Rd new file mode 100644 index 00000000..5cd47904 --- /dev/null +++ b/man/S7_group_generics.Rd @@ -0,0 +1,52 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/method-group.R +\name{S7_group_generics} +\alias{S7_group_generics} +\alias{S7_Math} +\alias{S7_Ops} +\alias{S7_Complex} +\alias{S7_Summary} +\alias{find_base_generic} +\title{S7 Group Generics} +\usage{ +S7_Math(x, ..., .Generic) + +S7_Ops(e1, e2, ..., .Generic) + +S7_Complex(z, ..., .Generic) + +S7_Summary(x, ..., na.rm = FALSE, .Generic) + +find_base_generic(.Generic) +} +\arguments{ +\item{x, z, e1, e2}{Objects used for dispatch.} + +\item{..., na.rm}{Additional arguments passed to methods.} + +\item{.Generic}{The name of the generic being dispatched on, i.e. if you've +defined a method for \code{S7_Math} and the user calls \code{abs()} then \code{.Generic} +will be \code{"abs"}. + +Use \code{find_base_generic()} to find the base generic that corresponds to the +generic name.} +} +\description{ +Group generics allow you to implement methods for many generics at once. +You cannot call a group generic directly; instead it is called automatically +by members of the group if a more specific method is not found. For example, +if you define a method for the \code{S7_Math} group generic, it will be called +when you call \code{abs()}, \code{sign()}, \code{sqrt()}, and many other similar generics +(see below for a complete list). +} +\section{Methods}{ +The group generics contain the following methods: +\itemize{ +\item \code{Ops}: \code{+}, \code{-}, \code{*}, \code{^}, \code{\%\%}, \code{\%/\%}, \code{/}, \code{==}, \code{>}, \code{<}, \code{!=}, \code{<=}, \code{>=}, \code{&}, \code{|} +\item \code{Math}: \code{abs}, \code{sign}, \code{sqrt}, \code{ceiling}, \code{floor}, \code{trunc}, \code{cummax}, \code{cummin}, \code{cumprod}, \code{cumsum}, \code{exp}, \code{expm1}, \code{log}, \code{log10}, \code{log2}, \code{log1p}, \code{cos}, \code{cosh}, \code{sin}, \code{sinh}, \code{tan}, \code{tanh}, \code{acos}, \code{acosh}, \code{asin}, \code{asinh}, \code{atan}, \code{atanh}, \code{cospi}, \code{sinpi}, \code{tanpi}, \code{gamma}, \code{lgamma}, \code{digamma}, \code{trigamma}, \code{round}, \code{signif} +\item \code{Summary}: \code{max}, \code{min}, \code{range}, \code{prod}, \code{sum}, \code{any}, \code{all} +\item \code{Complex}: \code{Arg}, \code{Conj}, \code{Im}, \code{Mod}, \code{Re} +\item \code{matrixOps}: \code{\%*\%} +} +} + diff --git a/tests/testthat/_snaps/method-group.md b/tests/testthat/_snaps/method-group.md new file mode 100644 index 00000000..a827eeb3 --- /dev/null +++ b/tests/testthat/_snaps/method-group.md @@ -0,0 +1,8 @@ +# can provide Math group generic + + Code + abs(foo1(-1, 2)) + Condition + Error in `abs.default()`: + ! non-numeric argument to mathematical function + diff --git a/tests/testthat/test-method-group.R b/tests/testthat/test-method-group.R new file mode 100644 index 00000000..79682670 --- /dev/null +++ b/tests/testthat/test-method-group.R @@ -0,0 +1,21 @@ +test_that("can provide Math group generic", { + local_methods(S7_Math) + foo1 <- new_class("foo1", properties = list(x = class_double, y = class_double)) + foo2 <- new_class("foo2", class_double) + + # base behaviour + expect_snapshot(abs(foo1(-1, 2)), error = TRUE) + expect_equal(abs(foo2(c(-1, 2))), foo2(c(1, 2))) + + method(S7_Math, foo1) <- function(x, ..., .Generic) { + .Generic <- find_base_generic(.Generic) + foo1(.Generic(x@x, ...), .Generic(x@y, ...)) + } + expect_equal(abs(foo1(-1, 2)), foo1(1, 2)) + + method(S7_Math, foo2) <- function(x, ..., .Generic) { + .Generic <- find_base_generic(.Generic) + foo2(.Generic(S7_data(x, ...))) + } + expect_equal(abs(foo2(c(-1, 2))), foo2(c(1, 2))) +}) diff --git a/tests/testthat/test-method-ops.R b/tests/testthat/test-method-ops.R index d825b65e..4e0670da 100644 --- a/tests/testthat/test-method-ops.R +++ b/tests/testthat/test-method-ops.R @@ -96,6 +96,23 @@ test_that("Ops generics falls back to base behaviour", { expect_equal(1:2 + foo(1), "numeric-foo") }) +test_that("specific method overrides group generic", { + local_methods(base_ops[["+"]], S7_Ops) + + foo <- new_class("foo", class_integer) + + method(`+`, list(foo, foo)) <- function(e1, e2) { + foo(S7_data(e1) + S7_data(e2) + 100L) + } + method(S7_Ops, list(foo, foo)) <- function(e1, e2, .Generic) { + .Generic <- find_base_generic(.Generic) + foo(.Generic(S7_data(e1), S7_data(e2))) + } + + expect_equal(foo(1L) * foo(1:5), foo(1:5)) + expect_equal(foo(1L) + foo(1:5), foo(1:5 + 1L + 100L)) +}) + test_that("`%*%` dispatches to S7 methods", { skip_if(getRversion() < "4.3") local_methods(base_ops[["+"]])