R通用分派到连接的环境

ffscu2ro  于 2022-12-20  发布在  其他
关注(0)|答案(2)|浏览(191)

我有一堆函数,我试图通过在一个环境中定义它们并附加环境来保持工作区的整洁,其中一些函数是S3泛型,它们似乎不适合这种方法。
我所遇到的最小示例需要4个文件:
试验机R

ttt.xxx <- function(object) print("x")

ttt <- function(object) UseMethod("ttt")

ttt2 <- function() {
  yyy <- structure(1, class="xxx")
  ttt(yyy)
}

在testfun.R中,我定义了一个S3泛型ttt和一个方法ttt.xxx,我还定义了一个调用泛型的函数ttt2。
试验剂R

test_env <- new.env(parent=globalenv()) 
source("testfun.R", local=test_env) 
attach(test_env)

在testenv.R中,我将testfun.R源代码添加到一个环境中,并附加到该环境中。
测试1.R

source("testfun.R")
ttt2()
xxx <- structure(1, class="xxx")
ttt(xxx)

test1.R将testfun.R提供给全局环境。ttt2和直接函数调用都可以工作。
测试2.R

source("testenv.R")
ttt2()
xxx <- structure(1, class="xxx")
ttt(xxx)

test2.R使用“attach”方法。ttt2仍然工作(并将“x”打印到控制台),但直接函数调用失败:

Error in UseMethod("ttt") : 
  no applicable method for 'ttt' applied to an object of class "xxx"

但是,不带参数调用ttt和ttt.xxx表明它们是已知的,ls(pos=2)表明它们在搜索路径上,sloop::s3_dispatch(ttt(xxx))告诉我它应该工作。
这个问题与Confusion about UseMethod search mechanism和其中的链接https://blog.thatbuthow.com/how-r-searches-and-finds-stuff/有关,但我无法理解正在发生的事情:为什么它不起作用,我怎样才能让它起作用。
我已经尝试了R Studio和R in the shell。
更新:根据以下答案,我将我的测试版本变更为:

test_env <- new.env(parent=globalenv()) 
source("testfun.R", local=test_env) 
attach(test_env)
if (is.null(.__S3MethodsTable__.))
  .__S3MethodsTable__. <- new.env(parent = baseenv())
for (func in grep(".", ls(envir = test_env), fixed = TRUE, value = TRUE))
  .__S3MethodsTable__.[[func]] <- test_env[[func]]
rm(test_env, func)

...这是有效的(我只使用“.”作为S3调度分隔符)。

kyks70gy

kyks70gy1#

一个鲜为人知的事实是,您必须使用.S3method()为定制环境内(包外)的S3泛型定义方法。1几乎没有人知道这一点的原因是因为它在全局环境中是 * 不 * 必要的;但从R3.6版开始,在其他任何地方都必须这样做。
实际上没有关于这个变化的文档,只有a technical blog post by Kurt Hornik about some of the background。注意,博客文章说这个变化是在R3.5.0中做的;然而,您所观察到的实际效果--S3方法不再在连接环境中搜索--只是在R3.6.0中才开始出现;在此之前,它不知怎么还没有激活。
...除了仅仅使用.S3method * 不会 * 修复你的代码,因为你的 * 调用环境 * 是全局环境。我不明白为什么这个不起作用的确切原因,我怀疑这是由于R的S3方法查找中的一个微妙的bug。事实上,使用getS3method('ttt', 'xxx') * 确实 * 起作用,即使它应该与实际的S3方法查找具有相同的行为。
我发现,实现此功能的唯一方法是将以下代码添加到testenv.R

if (is.null(.__S3MethodsTable__.)) {
    .__S3MethodsTable__. <- new.env(parent = baseenv())
}

.__S3MethodsTable__.$ttt.xxx <- ttt.xxx

...换句话说:手动为.GlobalEnv提供S3方法查找表。不幸的是,这依赖于未记录的S3实现细节,理论上将来可能会更改。
或者,如果您使用‘box’ modules而不是source,它“只是工作”,也就是说,您可以将整个testenv.R替换为以下内容:

box::use(./testfun[...])

这段代码将testfun.R视为一个 * 本地模块 * 并加载它,附加所有导出的名称(通过attach声明[...])。
1(在包内部,您需要使用等效的S3method命名空间声明,尽管如果您使用'roxygen 2',那么它会为您处理)

wljmcqd8

wljmcqd82#

首先,我的建议是:不要试图重新发明R软件包。它们解决了所有你说要解决的问题,也解决了其他问题。
其次,我将尝试解释test2.R中的错误,它在xxx对象上调用ttt,并且ttt.xxx在搜索列表中,但是没有找到。
问题是搜索ttt.xxx是如何发生的。搜索 * 不 * 在搜索列表中查找ttt.xxx,而是在调用ttt的环境中查找,然后在名为.__S3MethodsTable__.的对象中查找。我认为有两个原因:

  • 首先,它的速度要快得多,只需要在一两个地方进行查找,而且每当附加或分离一个包时,表都可以更新,这是一个相对罕见的操作。
  • 第二,它更可靠。每个包都有自己的方法表,因为两个包可以使用相同的名称来表示彼此无关的泛型,或者可以使用相同的类名来表示彼此无关的泛型。所以包代码需要能够首先找到自己的定义。

因为你对ttt()的调用发生在顶层,R首先在顶层查找ttt.xxx(),但是没有找到,然后在全局.__S3MethodsTable__.(实际上在基础环境中)查找,也没有找到,所以失败了。
有一种变通方法可以使您的代码正常工作。如果运行

.__S3MethodsTable__. <- list2env(list(ttt.xxx = ttt.xxx))

作为testenv.R的最后一行,那么您将在全局环境中创建一个方法表。(通常那里没有方法表,因为那是用户空间,R不喜欢把东西放在那里,除非用户要求。)R将找到那个方法表,并将找到它定义的ttt.xxx方法。如果这破坏了S3调度的其他方面,我不会感到惊讶。所以我不建议这么做,但是如果你坚持要重新发明软件包系统,那就试试吧。

相关问题