Wrong double dispatch method when putting a vctrs-built class in a package

225 Views Asked by At

I've created a new class to print percentages with vctrs, like explained in https://vctrs.r-lib.org/articles/s3-vector.html . It works well when I source the .R file. But when I build the package with devtools, basic operations made possible with vec_arith don't work anymore :

pct(0.5, 1L) + pct(0.25, 2L)
#Error: <pct0> + <pct0> is not permitted

However, similar cases are working well : conversions are good ; vec_math is also OK, it's possible to do sum(pct(0.5, 1L) + pct(0.25, 2L))

I've made a reproducible example, which contains the code necessary to load a small package with the percent class using devtools 2.3.2 and vctrs 0.3.6.

  1. We create a new package named percenterror with devtools :
create_package("D:\\Statistiques\\Packages\\percenterror")

On the console inside the RStudio project of this package, we declare dependencies and create a new .R file :

library(devtools)
use_package("vctrs",     min_version = "0.3.6")
use_r("percent_class")
  1. Let's copy the following code in the .R file that opens, with roxygen tags to generate the NAMESPACE :
#'  Import vctrs in NAMESPACE
#' @import vctrs
#' @keywords internal
#' @name vctrs
NULL

#' Create a vector of class pct using vctrs
#' @param x A double vector.
#' @param digits The number of digits to print. It can then be changed
#' with \code{\link{set_digits}}.
#'
#' @return A numeric vector of class pct.
#' @export
pct <- function(x = double(), digits = 0L) {
  x <- vctrs::vec_cast(x, double()) #take anything coercible as a double
  digits <- vctrs::vec_recycle(vctrs::vec_cast(digits, integer()), 1L)
  new_pct(x, digits)
}

#' @describeIn pct A constructor for class pct.
#' @export
new_pct <- function(x = double(), digits = 0L) {
  vctrs::vec_assert(x, double()) #check type or size
  vctrs::vec_assert(digits, ptype = integer(), size = 1)
  vctrs::new_vctr(x, digits = digits, class = "pct", inherit_base_type = TRUE) #"vctrs_pct"
}

#' Get and set number of digits of vectors with class pct
#' @param x A vector of class \code{\link{pct}} or \code{\link{decimal}}
#' @return \code{\link{get_digits}} : an integer vector with the number of digits.
#' @export
get_digits <- function(x) as.integer(attr(x, "digits"))
#' @rdname get_digits
#' @param value The number of digits to print, as an integer.
#' @return \code{\link{set_digits}} : a vector with the right number of digits.
set_digits <- function(x, value) `attr<-`(x, "digits", as.integer(value))

#' A vec_arith method to allow basic operations for pct
#' @param op Arithmetic operation to do.
#' @param x Pct object.
#' @param y Second object.
#'
#' @export
vec_arith.pct <- function(op, x, y, ...) {
  UseMethod("vec_arith.pct", y)
}

#' @export
vec_arith.pct.default <- function(op, x, y, ...) {
  vctrs::stop_incompatible_op(op, x, y)
}


#' @export
vec_arith.pct.pct <- function(op, x, y, ...) {
  new_pct(vctrs::vec_arith_base(op, x, y),
          digits = max(get_digits(x), get_digits(y)))
}

#' @export
vec_arith.pct.numeric <- function(op, x, y, ...) {
  new_pct(vctrs::vec_arith_base(op, x, y),
          digits = max(get_digits(x), get_digits(y)))
}

#' @export
vec_arith.numeric.pct <- function(op, x, y, ...) {
  new_pct(vctrs::vec_arith_base(op, x, y),
          digits = max(get_digits(x), get_digits(y)))
}

If we do library(vctrs) and source this document, pct(0.5) + pct(0.25) works fine.

  1. But then, back in the console, we create documentation and load the package :
document()
load_all()

Here, additions don't work anymore :

pct(0.5, 1L) + pct(0.25, 2L)
#Error: <pct0> + <pct0> is not permitted

However the method is found :

sloop::s3_get_method("vec_arith.pct.pct")
# function(op, x, y, ...) {
#   new_pct(vctrs::vec_arith_base(op, x, y),
#           digits = max(get_digits(x), get_digits(y)))
# }
#<environment: namespace:percenterror>

The method seems linked to the right generic :

sloop::s3_methods_generic("vec_arith.pct")
# # A tibble: 3 x 4
# generic         class   visible source
# <chr>           <chr>   <lgl>   <chr>
# 1 vec_arith.pct default TRUE    percenterror
# 2 vec_arith.pct numeric TRUE    percenterror
# 3 vec_arith.pct pct     TRUE    percenterror

vec_arith.numeric method works with pct :

1 + pct(0.5)
# <pct0[1]>
#   [1] 150%

But the opposite is not true, vec_arith_pct method does'nt work with numeric :

pct(0.5) + 1
#Error: <pct0> + <double> is not permitted

When we run the trace of the error with rlang::last_trace(), we find that the pct + pct operation does'nt in fact go to the right method, and does'nt even care about the functions defined above, because vec_arith.default is used in place of vec_arith.pct :

# <error/vctrs_error_incompatible_op>
#   <pct0> + <pct0> is not permitted
# Backtrace:
#   x
# 1. \-vctrs:::`+.vctrs_vctr`(pct(0.5, 0L), pct(0.25000001))
# 2.   +-vctrs::vec_arith("+", e1, e2)
# 3.   \-vctrs:::vec_arith.default("+", e1, e2)
# 4.     \-vctrs::stop_incompatible_op(op, x, y)
# 5.       \-vctrs:::stop_incompatible(...)
# 6.         \-vctrs:::stop_vctrs(...)

What went wrong, and what to do to make it works ? I've tried with ou without vctrs:: calls, importing or not importing almost everything in NAMESPACE, but can't manage to find what happens.

Thanks

1

There are 1 best solutions below

2
Joe Roe On

You need to add roxygen2 @method tags to export your vec_arith. functions as methods:

#' A vec_arith method to allow basic operations for pct
#' @param op Arithmetic operation to do.
#' @param x Pct object.
#' @param y Second object.
#'
#' @method vec_arith pct
#' @export
vec_arith.pct <- function(op, x, y, ...) {
  UseMethod("vec_arith.pct", y)
}

#' @method vec_arith.pct default
#' @export
vec_arith.pct.default <- function(op, x, y, ...) {
  vctrs::stop_incompatible_op(op, x, y)
}

#' @method vec_arith.pct pct
#' @export
vec_arith.pct.pct <- function(op, x, y, ...) {
  new_pct(vctrs::vec_arith_base(op, x, y),
          digits = max(get_digits(x), get_digits(y)))
}

#' @method vec_arith.pct numeric
#' @export
vec_arith.pct.numeric <- function(op, x, y, ...) {
  new_pct(vctrs::vec_arith_base(op, x, y),
          digits = max(get_digits(x), get_digits(y)))
}

#' @method vec_arith.numeric pct
#' @export
vec_arith.numeric.pct <- function(op, x, y, ...) {
  new_pct(vctrs::vec_arith_base(op, x, y),
          digits = max(get_digits(x), get_digits(y)))
}

This requirement has been removed from vctrs' double dispatch mechanism for most generics, but for the time being it's still required for vec_arith(): https://github.com/r-lib/vctrs/issues/1287