问题描述
我在下面给出了一个简单的例子,我在一个 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)
被调用,并且该内部方法不允许 list
或 data.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)
再次,我们可以看到存在两种不同的方法,因此使用了内部方法。
我想出的肮脏解决方法是:
- 创建一个非内部泛型
*
- 明确定义
*.foo
和 - 明确定义
*.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 仍然选择了第一个对象的方法,没有发出警告。
这当然不是问题的真正解决方案,原因有很多:
- 覆盖算术运算的泛型似乎不是一个好主意,因为它可能会破坏代码。
- 我们还需要处理仍然不起作用的
data.frame(a = 1) * z
(这里我们需要覆盖Ops.data.frame
的现有代码。 - 我们不需要为每个算术运算编写方法。
{vctrs} 包应该可以帮助我们找到一个更简单、更安全的解决方案,也许它已经存在。可能值得在 Github 上提出问题。