【发布时间】:2012-11-25 05:21:56
【问题描述】:
在Software for Data Analysis: Programming with R一书中,John Chambers 强调,通常不应该为函数的副作用而编写函数;相反,一个函数应该在不修改其调用环境中的任何变量的情况下返回一个值。相反,使用 data.table 对象编写好的脚本应该特别避免使用 <- 的对象分配,通常用于存储函数的结果。
首先,是一个技术问题。想象一个名为proc1 的R 函数,它接受data.table 对象x 作为其参数(可能还有其他参数)。 proc1 返回 NULL,但使用 := 修改 x。据我了解,proc1 调用proc1(x=x1) 会复制x1 只是因为承诺的工作方式。但是,如下所示,原始对象x1 仍然被proc1 修改。为什么/这是怎么回事?
> require(data.table)
> x1 <- CJ(1:2, 2:3)
> x1
V1 V2
1: 1 2
2: 1 3
3: 2 2
4: 2 3
> proc1 <- function(x){
+ x[,y:= V1*V2]
+ NULL
+ }
> proc1(x1)
NULL
> x1
V1 V2 y
1: 1 2 2
2: 1 3 3
3: 2 2 4
4: 2 3 6
>
此外,使用proc1(x=x1) 似乎并不比直接在 x 上执行该过程慢,这表明我对 Promise 的模糊理解是错误的,并且它们以传递引用的方式工作:
> x1 <- CJ(1:2000, 1:500)
> x1[, paste0("V",3:300) := rnorm(1:nrow(x1))]
> proc1 <- function(x){
+ x[,y:= V1*V2]
+ NULL
+ }
> system.time(proc1(x1))
user system elapsed
0.00 0.02 0.02
> x1 <- CJ(1:2000, 1:500)
> system.time(x1[,y:= V1*V2])
user system elapsed
0.03 0.00 0.03
因此,鉴于将 data.table 参数传递给函数不会增加时间,这使得为 data.table 对象编写过程成为可能,同时结合了 data.table 的速度和函数的通用性。然而,鉴于 John Chambers 所说,函数不应该有副作用,用 R 编写这种类型的过程编程真的“可以”吗?为什么他认为副作用是“坏的”?如果我要忽略他的建议,我应该注意哪些陷阱?我能做些什么来编写“好”的 data.table 程序?
【问题讨论】:
-
修改参数在某些圈子中并没有被高度考虑,但在其他圈子中它不被认为是副作用(留下来指修改不在参数列表中的东西)。也就是说,我对这种行为很好奇。该函数没有注意到 [.data.table 正在修改参数?或者也许只有实际的赋值会触发局部变量的创建。
-
@MatthewLundberg 我会添加一个答案,但基本上
data.table故意偏离 R 的写时复制。data.table不是写时复制,即使在函数中也是如此。如果你真的想要复制一个20GB的data.table,你需要在函数的开头放置x=copy(x),或者在函数内部写上x=copy(x)[,y:=V1*V2]。 -
@MatthewLundberg 我认为这在大多数圈子中被认为是副作用。
-
@hadley 这与我所写的并不矛盾。在 C 代码中,一个修改过的参数被视为正常活动(查看标准库中的数百个示例)。 C++ 消除了对此类结构的需求,但仍然使用 C,并且仍然传递非常量参数。
-
@hadley Matthew 说,iiuc,将某些内容传递给要通过引用修改的函数与超出其范围并修改未传递的函数略有不同。后者肯定是副作用。在 C 中可能会更改全局变量,在 R 中创建或更改 .GlobalEnv 中的变量。有些人会说修改显式传入的参数比这更安全,并且不要使用副作用来描述,其他人会这样做。
标签: r data.table