`vec_arith` 未按预期调用

问题描述

我在下面给出了一个简单的例子,我在一个 double 对象上定义了一个类“foo”,我希望任何涉及此类对象的算术运算都将它从它的“foo”类中剥离并正常进行。

我可以部分使其工作,但不能稳健。见下文:

library(vctrs)

x <- new_vctr(42,class = "foo")

# then this won't work (expected)
x * 2
#> Error: <foo> * <double> is not permitted

# define vec_arith method
vec_arith.foo <- function(op,x,y,...) {
  print("we went there")
  # wrap x in vec_data to strip off the class,and forward to `vec_arith_base`
  vec_arith_base(op,vec_data(x),y)
}

# Now this works  
x * 2
#> [1] "we went there"
#> [1] 84

# but this doesn't,and doesn't go through vec_arith.foo
x * data.frame(a=1)
#> Warning: Incompatible methods ("*.vctrs_vctr","Ops.data.frame") for "*"
#> Error in x * data.frame(a = 1): non-numeric argument to binary operator

# while this works
42 * data.frame(a=1)
#>    a
#> 1 42

如何让 x * data.frame(a=1) 返回与 42 * data.frame(a=1) 相同的值

traceback() 不返回任何内容,所以我不确定如何调试。

解决方法

这是一个有趣的问题,引起了我的兴趣。我不是这个问题的专家,但我找到了让它工作的方法。这是一个相当肮脏的解决方法,没有真正的解决方案。使用 {vctrs} 包应该有更好的方法来解决这个问题。

这个问题很复杂,因为我们正在处理一个使用双重调度的内部泛型 *(参见 here)。重要的 part 是:

Ops 组中的泛型,包括双参数算法 和布尔运算符,如 - 和 &,实现了一种特殊类型的方法 派遣。他们根据两个参数的类型进行调度,即 称为双重调度。

事实证明,对于像 x * y 这样的调用,R 会同时查找此调用和 y * x。那么有三种可能的结果:

方法是一样的,所以不管用哪种方法。

方法不同,R回退到内部方法 有警告。

一个方法是内部方法,在这种情况下,R 调用另一个方法。

让我们在查看问题时记住这一点。我首先避免使用 {vctrs} 包,并尝试通过两种方式重构问题。首先,我尝试将新类的对象与列表相乘。这重现了原始示例中的错误:

# lets create a new object
x1 <- 10
class(x1) <- "myclass"

# and multiply it with a list
l <- list(1)    
x1 * l 

# same error as in orignal example,but without warning
#> Error in x1 * l: non-numeric argument to binary operator

sloop::s3_dispatch(x1 * l)
#>    *.myclass
#>    *.default
#>    Ops.myclass
#>    Ops.default
#> => * (internal)

sloop::s3_dispatch(l * x1)
#>    *.list
#>    *.default
#>    Ops.list
#>    Ops.default
#> => * (internal)

我们可以通过 {sloop} 包看到调用了内部泛型。对于这种泛型,无法在列表上使用 *。所以让我们试试我们是否可以覆盖这个方法:

`*.myclass` <- function(x,y) {
  print("myclass")
  if (is.list(y)) {
    print("if clause")
    y <- unlist(y)
  } else {
    print("didn't use if clause")
  }
  
    x + y # to see if it's working the operation is changed
}

x1 * l # now working
#> [1] "myclass"
#> [1] "if clause"
#> [1] 11
#> attr(,"class")
#> [1] "myclass"

sloop::s3_dispatch(x1 * l)
#> => *.myclass
#>    *.default
#>    Ops.myclass
#>    Ops.default
#>  * * (internal)

sloop::s3_dispatch(l * x1)
#>    *.list
#>    *.default
#>    Ops.list
#>    Ops.default
#> => * (internal)

这行得通(虽然我们真的不应该改变方法调用中的对象)。这里我们现在有了上面描述的第三种情况:方法不同,一种是内部的,所以调用的是非内部的方法。与 data.frame 不同,list 没有现有的算术运算方法。所以我们需要一个例子,其中两个不同类的对象相乘,方法不同。

# another object
y1 <- 20
class(y1) <- "another_class"

# here we still only have one method `*.myclass`:
x1 * y1 # working
#> [1] "myclass"
#> [1] "didn't use if clause"
#> [1] 30
#> attr(,"class")
#> [1] "myclass"

sloop::s3_dispatch(x1 * y1)
#> => *.myclass
#>    *.default
#>    Ops.myclass
#>    Ops.default
#>  * * (internal)

sloop::s3_dispatch(y1 * x1)
#>    *.another_class
#>    *.default
#>    Ops.another_class
#>    Ops.default
#> => * (internal)

# lets introduce another method:    
`*.another_class` <- function(x,y) {
  x - y # again,to see if it is working we change the operation
}

# now we get (only) a warning,but with a different result!
x1 * y1 
#> Warning: Incompatible methods ("*.myclass","*.another_class") for "*"
#> [1] 200
#> attr(,"class")
#> [1] "myclass"

sloop::s3_dispatch(x1 * y1)
#> => *.myclass
#>    *.default
#>    Ops.myclass
#>    Ops.default
#>  * * (internal)

sloop::s3_dispatch(y1 * x1)
#> => *.another_class
#>    *.default
#>    Ops.another_class
#>    Ops.default
#>  * * (internal)

这里我们现在有上面描述的第二种情况:两种方法不同,R回退到内部方法并警告。这会产生“未改变”的结果 20 * 10 = 200

所以对于最初的问题,我的理解是我们有两个相互冲突的方法“*.vctrs_vctr”和“Ops.data.frame”。为此,内部方法 * (internal) 被调用,并且该内部方法不允许 listdata.frame(这通常在未使用的 Ops.data.frame 内完成,因为方法冲突)。

library(vctrs)

z <- new_vctr(42,class = "foo")
a <- data.frame(a = 1)

z * a
#> Warning: Incompatible methods ("*.vctrs_vctr","Ops.data.frame") for "*"
#> Error in z * a: non-numeric argument to binary operator

sloop::s3_dispatch(z * a)
#>    *.foo
#> => *.vctrs_vctr
#>    *.default
#>    Ops.foo
#>    Ops.vctrs_vctr
#>    Ops.default
#>  * * (internal)

sloop::s3_dispatch(a * z)
#>    *.data.frame
#>    *.default 
#> => Ops.data.frame
#>    Ops.default
#>  * * (internal)

再次,我们可以看到存在两种不同的方法,因此使用了内部方法。

我想出的肮脏解决方法是:

  1. 创建一个非内部泛型 *
  2. 明确定义 *.foo
  3. 明确定义 *.numeric,一旦对象被 vec_data() “未分类”,它将被调用。
`*` <- function(x,y) {
  UseMethod("*")
}

`*.foo` <- function(x,y) {
  op_fn <- getExportedValue("base","*")
  op_fn(vec_data(x),vec_data(y))
}

`*.numeric` <- function(x,y) {
  print("numeric")
  fn <- getExportedValue("base","*")
  fn(x,y)
}

z * a
#> [1] "numeric"
#>    a
#> 1 42

sloop::s3_dispatch(z * a)
#> => *.foo
#>  * *.vctrs_vctr
#>    *.default
#>    Ops.foo
#>    Ops.vctrs_vctr
#>    Ops.default
#>  * * (internal)

sloop::s3_dispatch(a * z)
#>    *.data.frame
#>    *.default
#> => Ops.data.frame
#>    Ops.default
#>  * * (internal)

reprex package (v0.3.0) 于 2021 年 1 月 13 日创建

不幸的是,我不能 100% 确定发生了什么。似乎覆盖了 * 泛型,也覆盖了 R 处理这种泛型的双重调度的方式。让我们重新审视上面两种不同类型的对象 x1 * y1 的乘法。早些时候,这两种方法都被调用,由于它们不同,发出了警告并选择了内部方法。现在我们观察到以下几点:

x1 * y1 # working without warning
#> [1] "myclass"
#> [1] "didn't use if clause"
#> [1] 30
#> attr(,"class")
#> [1] "myclass"

sloop::s3_dispatch(x1 * y1)
#> => *.myclass
#>    *.default
#>    Ops.myclass
#>    Ops.default
#>  * * (internal)

sloop::s3_dispatch(y1 * x1)
#> => *.another_class
#>    *.default
#>    Ops.another_class
#>    Ops.default
#>  * * (internal)

我们有两个冲突的方法,而且 R 仍然选择了第一个对象的方法,没有发出警告。

这当然不是问题的真正解决方案,原因有很多:

  1. 覆盖算术运算的泛型似乎不是一个好主意,因为它可能会破坏代码。
  2. 我们还需要处理仍然不起作用的 data.frame(a = 1) * z(这里我们需要覆盖 Ops.data.frame 的现有代码。
  3. 我们不需要为每个算术运算编写方法。

{vctrs} 包应该可以帮助我们找到一个更简单、更安全的解决方案,也许它已经存在。可能值得在 Github 上提出问题。