在存储级别,公式是解析树.解析树使用`~`()一个或两个参数对函数的调用进行编码.对于单侧公式,它需要一个表示RHS的参数,而对于双侧公式,它需要两个参数来表示公式的LHS和RHS.
应该提到的`~`()是,嵌入在公式的解析树存储表示中的调用实际上并不意味着什么.通常,`~`()除了允许创建公式对象之外,该函数实际上不做任何事情,无论是显式(例如`~`(a+b,c/d))还是使用R语言提供的语法糖特征(例如a+b~c/d).`~`()在公式的解析树存储表示的顶级编码中使用该函数是相当随意且无关紧要的实现细节.我稍后会对此进行扩展.
R语言可以将解析树分解为递归列表结构,这可以帮助我们检查和理解这些解析树的结构.
我写了一个简短的递归函数,可以做到这一点:
ptunwrap <- function(x) if (typeof(x)=='language') lapply(as.list(x),ptunwrap) else x;
Run Code Online (Sandbox Code Playgroud)
那么让我们看一个例子:
f1 <- a+b~c/d;
f1;
## a + b ~ c/d
ptunwrap(f1);
## [[1]]
## `~`
##
## [[2]]
## [[2]][[1]]
## `+`
##
## [[2]][[2]]
## a
##
## [[2]][[3]]
## b
##
##
## [[3]]
## [[3]][[1]]
## `/`
##
## [[3]][[2]]
## c
##
## [[3]][[3]]
## d
##
Run Code Online (Sandbox Code Playgroud)
当解析树包含函数调用时,函数调用在递归列表结构中表示为单个列表节点.函数的符号作为该列表的第一个列表组件嵌入,其参数作为列表的后续列表组件嵌入.
上面的解析树中有三个函数调用.顶级列表表示对`~`()函数的调用,如前所述.第二个顶级列表组件进一步分支到另一个列表,该列表由对`+`()函数的调用组成,该函数本身带有两个参数,即符号a和b.第三个组件是类似的,表示对`/`()函数的调用,并再次采用两个参数,符号c和d.
重要的是要理解尽管解析树总是表示语法上有效的R代码,并且能够被评估以产生单个结果值,但是没有必要评估解析树.完全可以创建一个解析树而不会对它进行评估.
那么,创建一个永远不会评估的解析树的目的是什么?在R中,这通常是为了利用方便的语法促进某些信息片段与函数的通信.
作为一个随机的例子,data.table包允许以下语法糖使用:=运算符将列添加到现有data.table :
library(data.table);
dt <- data.table(a=1:3);
dt[,b:=a*2L];
dt;
## a b
## 1: 1 2
## 2: 2 4
## 3: 3 6
Run Code Online (Sandbox Code Playgroud)
在内部,data.table使用非标准参数求值来检索第二个参数的解析树(技术上在函数定义中为第三个;在语法 - 糖形式中为第二个)到`[.data.table`()函数,通常称为" j参数",因为参数名称是j.如果你愿意,你可以直接在R中检查源本身.这里是最相关的代码段的片段:
data.table:::`[.data.table`;
## function (x, i, j, by, keyby, with = TRUE, nomatch = getOption("datatable.nomatch"),
## mult = "all", roll = FALSE, rollends = if (roll == "nearest") c(TRUE,
## TRUE) else if (roll >= 0) c(FALSE, TRUE) else c(TRUE,
## FALSE), which = FALSE, .SDcols, verbose = getOption("datatable.verbose"),
## allow.cartesian = getOption("datatable.allow.cartesian"),
## drop = NULL, on = NULL)
## {
##
## ... snip ...
##
## if (!missing(j)) {
## jsub = substitute(j)
## if (is.call(jsub))
## jsub = construct(deconstruct_and_eval(replace_dot(jsub),
## parent.frame(), parent.frame()))
## }
##
## ... snip ...
##
Run Code Online (Sandbox Code Playgroud)
我们可以看到他们正在使用它substitute(j)来检索j参数的解析树.对于上面的演示,这是他们将得到的:
ptunwrap(substitute(b:=a*2L));
## [[1]]
## `:=`
##
## [[2]]
## b
##
## [[3]]
## [[3]][[1]]
## `*`
##
## [[3]][[2]]
## a
##
## [[3]][[3]]
## [1] 2
##
Run Code Online (Sandbox Code Playgroud)
稍后在代码中,他们测试顶级函数符号是否:=为操作符,该操作符是支持向data.table添加(或修改或删除RHS为NULL)列的操作符.如果是这样,他们会测试LHS是否包含单个裸字,该裸字被视为要添加(或修改或删除)的列的名称.实际上,在这种情况下,它们实际上不可能实际评估解析树的LHS,因为它由data.table中尚不存在的符号组成.但是,RHS最终会被评估以生成要添加到新名称下的data.table的列向量.
因此,应该清楚公式可以在R中的各种上下文中使用,并且它们并不总是被评估.有时,只需检查解析树以检索从调用者传递给被调用者的信息.即使在上下文在那里它们被评估,有时仅LHS或RHS(或两者)将被独立评估,忽略在它被创建时嵌入在分析树的顶层功能符号.
转到该boxplot()函数,让我们看一下有关formula参数的文档:
公式,例如y~grp,其中y是要根据分组变量grp(通常是因子)分成组的数据值的数值向量.
在这种情况下,公式的两边最终被独立评估,LHS提供数据向量,RHS提供分组定义.
证明这一点的好方法如下:
boxplot(1:9~1:9%%3L);
Run Code Online (Sandbox Code Playgroud)
注意公式的两边是如何由文字表达式组成的:
1:9;
## [1] 1 2 3 4 5 6 7 8 9
1:9%%3L;
## [1] 1 2 0 1 2 0 1 2 0
Run Code Online (Sandbox Code Playgroud)
在内部,boxplot()必须独立评估公式的每一侧以生成数据和分组向量,几乎就像您已将两个表达式作为单独的参数传递一样.
那么,让我们创建一个月度时间序列箱图的简单演示:
N <- 36L; df <- data.frame(date=seq(as.Date('2016-01-01'),by='month',len=N),y=rnorm(N));
df;
## date y
## 1 2016-01-01 -1.56004488
## 2 2016-02-01 0.65699747
## 3 2016-03-01 0.05729631
## 4 2016-04-01 -0.02092276
## 5 2016-05-01 0.46673530
## 6 2016-06-01 -0.18652580
## 7 2016-07-01 0.06228650
## 8 2016-08-01 1.54452267
## 9 2016-09-01 1.06643594
## 10 2016-10-01 -1.51178160
## 11 2016-11-01 0.82904673
## 12 2016-12-01 0.37667201
## 13 2017-01-01 -0.10135801
## 14 2017-02-01 0.94692462
## 15 2017-03-01 -1.60781946
## 16 2017-04-01 0.47189753
## 17 2017-05-01 -1.32869317
## 18 2017-06-01 -0.49821455
## 19 2017-07-01 0.54474606
## 20 2017-08-01 0.47565264
## 21 2017-09-01 -0.97494730
## 22 2017-10-01 -1.22781588
## 23 2017-11-01 -0.34919086
## 24 2017-12-01 -0.78153843
## 25 2018-01-01 -0.59355220
## 26 2018-02-01 -2.58287605
## 27 2018-03-01 1.42148186
## 28 2018-04-01 -1.01278176
## 29 2018-05-01 -0.80961662
## 30 2018-06-01 0.19793126
## 31 2018-07-01 -1.03072915
## 32 2018-08-01 -0.87896416
## 33 2018-09-01 -2.36216655
## 34 2018-10-01 1.82708221
## 35 2018-11-01 0.05579195
## 36 2018-12-01 1.28612246
boxplot(y~months(date),df);
Run Code Online (Sandbox Code Playgroud)
如果需要,您可以学习源代码,这需要跟踪S3查找过程:
boxplot;
## function (x, ...)
## UseMethod("boxplot")
## <bytecode: 0x600b50760>
## <environment: namespace:graphics>
graphics:::boxplot.formula;
## function (formula, data = NULL, ..., subset, na.action = NULL)
## {
## if (missing(formula) || (length(formula) != 3L))
## stop("'formula' missing or incorrect")
## m <- match.call(expand.dots = FALSE)
## if (is.matrix(eval(m$data, parent.frame())))
## m$data <- as.data.frame(data)
## m$... <- NULL
## m$na.action <- na.action
## m[[1L]] <- quote(stats::model.frame)
## mf <- eval(m, parent.frame())
## response <- attr(attr(mf, "terms"), "response")
## boxplot(split(mf[[response]], mf[-response]), ...)
## }
## <bytecode: 0x6035c67f8>
## <environment: namespace:graphics>
Run Code Online (Sandbox Code Playgroud)
这几乎是环岛令人恼怒和复杂的,但graphics:::boxplot.formula()有效的检索(通过match.call()),导致其自身的调用,按摩解析树了一点,最明显的是更换了自己的函数符号boxplot.formula用stats::model.frame,然后评估新的分析树,从而调用stats::model.frame().该函数本身非常复杂,涉及进一步的S3查找,但这里是最相关的代码:
model.frame;
## function (formula, ...)
## UseMethod("model.frame")
## <bytecode: 0x601464b18>
## <environment: namespace:stats>
model.frame.default;
## function (formula, data = NULL, subset = NULL, na.action = na.fail,
## drop.unused.levels = FALSE, xlev = NULL, ...)
## {
##
## ... snip ...
##
## if (!inherits(formula, "terms"))
## formula <- terms(formula, data = data)
## env <- environment(formula)
## rownames <- .row_names_info(data, 0L)
## vars <- attr(formula, "variables")
## predvars <- attr(formula, "predvars")
## if (is.null(predvars))
## predvars <- vars
## varnames <- sapply(vars, function(x) paste(deparse(x, width.cutoff = 500),
## collapse = " "))[-1L]
## variables <- eval(predvars, data, env)
##
## ... snip ...
##
Run Code Online (Sandbox Code Playgroud)
因此,最终,它从公式对象中检索单个表达式,并使用公式eval()的给定data.frame和闭包环境作为上下文来计算它们,从而得到结果向量:
attr(terms(y~months(date),data=df),'variables');
## list(y, months(date))
eval(attr(terms(y~months(date),data=df),'variables'),df);
## [[1]]
## [1] -1.56004488 0.65699747 0.05729631 -0.02092276 0.46673530 -0.18652580 0.06228650 1.54452267 1.06643594 -1.51178160 0.82904673 0.37667201 -0.10135801 0.94692462 -1.60781946 0.47189753 -1.32869317 -0.49821455 0.54474606
## [20] 0.47565264 -0.97494730 -1.22781588 -0.34919086 -0.78153843 -0.59355220 -2.58287605 1.42148186 -1.01278176 -0.80961662 0.19793126 -1.03072915 -0.87896416 -2.36216655 1.82708221 0.05579195 1.28612246
##
## [[2]]
## [1] "January" "February" "March" "April" "May" "June" "July" "August" "September" "October" "November" "December" "January" "February" "March" "April" "May" "June" "July"
## [20] "August" "September" "October" "November" "December" "January" "February" "March" "April" "May" "June" "July" "August" "September" "October" "November" "December"
##
Run Code Online (Sandbox Code Playgroud)
要重申先前提出的观点,请注意`~`()在评估过程中无法找到该功能.它是R公式的任意实现细节,它们将`~`()函数编码为公式对象的分析树存储表示中的顶级函数符号.如果出现公式的一方(s)的实际评估,则不涉及对该函数的评估.
最后,让我们考虑如果您实际评估包含公式的存储表示的整个解析树会发生什么.回想一下,该`~`()函数只是从其参数创建一个公式.因此,评估一个公式有一个有趣的效果,即吐出刚刚评估的相同公式:
f1;
## a + b ~ c/d
eval(f1);
## a + b ~ c/d
eval(eval(f1));
## a + b ~ c/d
Run Code Online (Sandbox Code Playgroud)
我在解析树和公式上写了几个其他答案,你可能会感兴趣:
| 归档时间: |
|
| 查看次数: |
868 次 |
| 最近记录: |