local()与R中其他闭包方法有何不同?

17

昨天我从Bill Venables那里学到了local()如何帮助创建静态函数和变量,例如:

example <- local({
  hidden.x <- "You can't see me!"
  hidden.fn <- function(){
    cat("\"hidden.fn()\"")
  }
  function(){
    cat("You can see and call example()\n")
    cat("but you can't see hidden.x\n")
    cat("and you can't call ")
    hidden.fn()
    cat("\n")
  }
})

该命令在命令提示符下的行为如下:

> ls()
[1] "example"
> example()
You can see and call example()
but you can't see hidden.x
and you can't call "hidden.fn()"
> hidden.x                 
Error: object 'hidden.x' not found
> hidden.fn()
Error: could not find function "hidden.fn"

我在R中的静态变量讨论中看到过这种情况,并采用了不同的方法。这两种方法各有优缺点,请问您觉得哪个更好呢?
2个回答

12

封装

这种编程风格的优点在于隐藏的对象不太可能被其他任何东西覆盖,因此您可以更加自信地认为它们包含了您想要的内容。由于无法轻易访问它们,它们也不会被错误地使用。在问题中链接的帖子中,有一个全局变量count,它可以从任何地方访问并覆盖,因此如果我们在调试代码并查看count时发现其值已更改,则无法确定哪部分代码已更改它。相比之下,在问题的示例代码中,我们更有把握确保没有其他代码部分参与其中。

请注意,我们实际上可以访问隐藏的函数,尽管这不是那么容易:

# run hidden.fn
environment(example)$hidden.fn()

面向对象编程

需要注意的是,这非常接近面向对象编程,其中examplehidden.fn是方法,hidden.x是属性。我们可以这样明确地表示:

library(proto)
p <- proto(x = "x", 
  fn = function(.) cat(' "fn()"\n '),
  example = function(.) .$fn()
)
p$example() # prints "fn()"

proto并没有隐藏xfn,但不容易因失误访问它们,因为您必须使用 p$xp$fn() 来访问它们,这与能够写 e <- environment(example); e$hidden.fn() 并没有太大区别。

编辑:

面向对象的方法确实增加了继承的可能性,例如可以定义一个p的子类,它的功能类似于p,但覆盖了fn

ch <- p$proto(fn = function(.) cat("Hello from ch\n")) # child
ch$example() # prints: Hello from ch

6

local()可以实现单例模式--例如,snow包使用它来跟踪用户可能创建的单个Rmpi实例。

getMPIcluster <- NULL
setMPIcluster <- NULL
local({
    cl <- NULL
    getMPIcluster <<- function() cl
    setMPIcluster <<- function(new) cl <<- new
})

local()也可以用于在脚本中管理内存,例如,在子句的最后一行分配大型中间对象以创建最终对象所需。当local返回时,这些大型中间对象可供垃圾回收。

使用函数创建闭包是一种工厂模式--在R文档的银行账户示例中,每次调用open.account都会创建一个新的账户。

正如@otsaw所提到的,记忆化可能使用local来实现,例如,在爬虫中缓存网站。

library(XML)
crawler <- local({
    seen <- new.env(parent=emptyenv())
    .do_crawl <- function(url, base, pattern) {
        if (!exists(url, seen)) {
            message(url)
            xml <- htmlTreeParse(url, useInternal=TRUE)
            hrefs <- unlist(getNodeSet(xml, "//a/@href"))
            urls <-
                sprintf("%s%s", base, grep(pattern, hrefs, value=TRUE))
            seen[[url]] <- length(urls)
            for (url in urls)
                .do_crawl(url, base, pattern)
        }
    }
    .do_report <- function(url) {
        urls <- as.list(seen)
        data.frame(Url=names(urls), Links=unlist(unname(urls)),
                   stringsAsFactors=FALSE)
    }
    list(crawl=function(base, pattern="^/.*html$") {
        .do_crawl(base, base, pattern)
    }, report=.do_report)
})

crawler$crawl(favorite_url)
dim(crawler$report())

“记忆化”的典型例子——斐波那契数列并不令人满意——不会超出 R 语言的数值表示范围,所以很可能会使用高效预计算值的查找表。有趣的是这里的爬虫是单例的;也可以采用工厂模式,每个基 URL 对应一个爬虫。”

1
local让记忆化(memoization)更加方便。在"The R Inferno"中有一个例子。 - otsaw
你对爬虫作为单例的想法很有趣,因为与本地方式的替代方案是立即评估一个没有参数的匿名函数 - 工厂模式可能会使用基础 URL 的闭包。 - hadley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接