80.ADR之Metaprogramming之expressions
前面介绍了NSE,也提到了expression,quote()返回的就是一个expr(简写下同),到底什么是expr呢,expr就是一个可以被R执行的动作,而quote()就可以捕捉这个动作,expr也被叫做abstract synatax tree(ast)抽象语法树,因为它也代表了R代码的结构层次树,所以你既可以把它理解为静态的代码结构层次树,也可以理解为动态的可以被R执行的动作.我们可以用pryr::ast来看,注意这个ast函数非常有用!后面经常用到!
<br />
#我们通过ast来看代码的结构树<br />
ast(y<-x*10)<br />
#或者用quote把这个动作捕捉下来<br />
quote(y<-x*10)<br />
而正如很多时候R语言的不一致性带来的困扰,expression()却不是返回一个expr,而是一个list of exprs,
关于quote()和expression()更多不同请看这个帖子.
正如我所说,ast是非常好的东西,我们可以用它看清楚很多东西:
ast(if(x>1) x else 1/x)<br />
#或者也许你想看看 if else if else 好的<br />
ast(if (x>3) 1 else if(x>2) 2 else 3)<br />
#感觉到这个函数牛x的你 也许会试验while<br />
ast( while( i>1) {a<-a+1})<br />
#同样你也可以看清楚for,其实一切控制在R中都是函数<br />
我们可以用ast干什么呢?既然都可以看到代码的结构了,还有什么不能做啊,比如看清楚if else的原理之后,结合if是个函数,基础再好点,写出下面不难
#如果前面你基础好,下面对你应该很熟悉<br />
"if"(3>1,2,3)
那么我也想写个for的函数,OK,用ast看下
<br />
ast( for( i in 1:3) {print(4)})<br />
我们发现函数name是for,第一个参数是i,第二个是一个1:3返回的向量,第三个是循环体
那我们写个"for"
"for"(i,1:3,print(4))
怎么样?ast神奇不!
熟悉了ast之后,我们再回到主线,讨论expr,通常一个expr可能有4种形式:constants,names,calls,pairlists.
下面我们结合ast来先大概看下这4个部分到底都是怎么样的:
1.constants
constants就是长度为1的atomic vectors,ast原样返回
ast("a")<br />
ast(1L)
2.names
就是通常对象的名字,ast返回名字前面加个backtik(反引号)
ast(mean)
3.calls
代表了调用一个函数的动作,它是递归的
ast(f(g(),h(1,a)))
4.pairlists
这是一个R的遗留的东西,很多地方被list取代了,除了函数参数中,ast会用[]标出
ast(function(x=1,y) x)
大致了解了要说的内容之后,先介绍几个有趣的知识,再来详细深入names,calls,pairlists(constants就u需要了吧)
第一个有趣的知识是
ast(x+y%+%z)<br />
ast(x^y%+%z)
这样我们就可以看出%+%之类的函数的优先权了
第二个有趣的知识见这里
就是substitute会插入calltree引起的一些问题
好,下面深入
1.names
对于string,我们可以用as.name转成名字,对于name我们用is.name判断,稍微注意下
a a这样的名字,用backtick反引一下
identical(quote(name),as.name("name"))
as.name的用途就在于当函数传的是字符的时候,我们需要处理字符,否则我们直接用quote更省事
假如我们要实现自己版本的get,assign,我们怎么做呢?我们用到的工具有as.name,substitute,eval,至于环境的问题,我们不作深入
get2=function(x){<br />
eval(as.name(x),parent.frame())<br />
}<br />
assign2=function(x,y){<br />
x<-as.name(x)<br />
tmp<-substitute(x<-y)<br />
eval(tmp,parent.frame())<br />
}<br />
#下面实验<br />
R>a=1<br />
R>get2("a")<br />
[1] 1<br />
R>yyyy<br />
Error: object 'yyyy' not found<br />
R>assign2("yyyy",2:4)<br />
R>yyyy<br />
[1] 2 3 4<br />
因为我们必须处理string,所以使用as.name,我们无法使用quote,还要注意的是assign2这个函数,其实我想了蛮久才写出来,最关键是对substitute本身以及3个规则的理解,用x它就会在当前环境中找,并用as.name(x)的结果替换,这点非常重要因为直接
assign2=function(x,y){<br />
tmp<-substitute(as.name(x)<-y)<br />
eval(tmp,parent.frame())<br />
}
是不行的!我觉得虽然代码很短,但是道理蛮深的,首先必须quote(a<-4)这个形式才能eval,而quote本身不能把a<-y,用4替换y,把这个形式变出来,因为它只会原样返回它的输入,所以得用substitute,而要完成类似substitute(a<-4),而不是substitute(as.name("a")<-4)因为我们不能as.name("a")<-4,因为as.name("name")和quote(name)效果一样,所以as.name("a")<-4相当于quote(name)<-4和我们原意不一样,所以我们用x=as.name("a"),然后substitute(x<-4)这样x就被as.name("a")的结果a替换掉了
对于names,除了上面最基本的应用,有个比较特别的名字,empty name,它用来代表missing arguments,但它不能被绑定到一个名字
f<-function(x) 10<br />
formals(f)$x<br />
is.name(formals(f)$x)<br />
as.character(formals(f)$x)<br />
a<-formals(f)$x
我们可以用formals(f)去修改f的参数
g<-function(x=20,y=x) {x+y}<br />
formals(g)
对于这个函数我们看到g的参数情况,如果我们想把y设置为没有默认的,那就相当于给个empty name给它,方式为
formals(g)$y<-quote(expr =)<br />
g
quote(expr =)是显示创建empty name的方法,这样的话,没有默认值到有默认值,有默认值到没有默认值,修改默认值都可以做了,一个函数的参数就可以被我们改动了,甚至我们可以增减:
formals(g)$z<-quote(expr=)<br />
g<br />
#function (x = 20, y = x, z)<br />
#{<br />
# x + y<br />
#}<br />
formals(g)$x<-NULL<br />
g<br />
#function (y = x, z)<br />
#{<br />
# x + y<br />
#}
2.calls
这部分是重点,一个call很像一个list,它有length,[[,[方法,并且是递归的,因为参数中可能也是一个call等
通常一个call的第一个部分是函数名或者另一个call
x<-quote(mean(1:10))<br />
x[[1]]<br />
x[[2]]<br />
x<-quote(add(10)(20))<br />
x[[1]]
其它部分就是参数了,它们可以通过位置或者名字取到,也可以通过$<-,[[<-来修改,这部分我就不写了,大家可以增加或者减少(NULL),自己试验下,和names中介绍的修改参数差不多
当然我们也可以通过[方法,x[-1]来修改,然后as.list(x[-1])这样就把参数显示转换为一个列表了
另外需要注意的是,函数通过位置修改要特别注意,这个位置到底是什么参数,因为R的关键字参数的概念,我们可以用f(x=3,4)之类的形式调用一个函数,这和f(4,x=3)效果是一样的,但是要注意这两个call的参数list是不同的,我们得小心的对付通过位置取参数的方式,如果你觉得很麻烦,作者介绍了standardise_call(),它内部调用match.call()来完成参数的标准化,具体看下例子
m1<-quote(read.delim("data.txt",sep="|"))<br />
m2<-quote(read.delim(s="|","data.txt"))<br />
standardise_call(m1)<br />
standardise_call(m2)
我们再看下standardise_call的源码
standardise_call<br />
function (call, env = parent.frame())<br />
{<br />
stopifnot(is.call(call))<br />
f <- eval(call[[1]], env)<br />
if (is.primitive(f))<br />
return(call)<br />
match.call(f, call)<br />
}<br />
<environment: namespace:pryr>
基本上就是取得函数名字的值,也就是函数对象,然后进行match.call(f,call)这个match.call()就帮我们标准化了,包括参数部分匹配等等问题全都自己解决了,至于match.call()的源码是什么,暂时先不管了
当然我们quote()可以返回一个call,我们也可以自己创建call,方法有两个:
第一,利用call(),它的第一个参数是函数名字,其它都是exprs代表的参数:
call("mean",quote(1:10),na.rm=TRUE)
这里有个问题,其它参数应该是表达式,所以下面是不同的:
<br />
a<-call("mean",1:10)<br />
b<-call("mean",quote(1:10))<br />
identical(a,b)<br />
关键就在于参数是不是expression了,第一个不是,第二个是,那么为什么第一个会形成一个call呢?不是说其它参数应该是expression吗?而na.rm=TRUE又是什么?
看这里的讨论
第二,利用as.call()将一个list变成一个call,list第一个参数要求是一个name或者一个call,其他的是参数
as.call(list(quote(mean),quote(1:10)))<br />
as.call(list(quote(add(10)),20))
知道了基本的call内容之后,我们来看看一些实例,这些实例都是ADR这部分的题目中选出的,基本上是我做出来供大家参考的,不会的没办法呈现给大家啦[s:11]
第一个问题:实现自己的do.call
do.call2=function(i,...){<br />
tmp= as.call(list(as.name(i),...))<br />
eval(tmp,parent.frame())<br />
}
第二个问题:实现如下concat()函数
concat(quote(f),a=1,b=quote(mean(a)))<br />
#f(a=1,b=mean(a))
concat<-function(fname,...){<br />
as.call(list(fname,...))<br />
}
第三个问题:利用list()传参,如下
<br />
make_call(quote(mean),list(quote(x),na.rm=TRUE))<br />
#mean(x,na.rm=TRUE)<br />
make_call(quote(mean),quote(x),na.rm=TRUE)<br />
#mean(x,na.rm=TRUE)
我自己把这个问题升级了,就是第一种方式和第二种混合也可以也就是说
make_call(quote(mean),list(quote(x),na.rm=TRUE),a=1,4,list(c=3))<br />
#mean(x, na.rm = TRUE, a = 1, 4, 3)
为了实现这个功能,我是花了蛮久的时间代码如下:
<br />
make_call<-function(fname,...){<br />
tmp=list(...)<br />
nametmp=names(tmp)<br />
tmp=lapply(tmp,function(x){<br />
if(!is.list(x))<br />
list(x)<br />
else<br />
x<br />
}<br />
)<br />
for( i in seq_along(tmp)) {<br />
names(tmp[[i]])[1]=nametmp[i]<br />
}<br />
transtmp=Reduce('c',tmp)<br />
as.call(c(fname,transtmp))</p>
<p>}
实现上述代码着实花了我不少功夫,最大的技巧是利用c这个函数去合并list,这个细节非常有用,这在我ADR系列的第一个帖子中就有提到,这也是transtmp=Reduce('c',tmp)代码的由来,它遍历包含list的list,tmp,然后把那些list给合并在一起,然后怎么得到tmp呢,就是对参数list(...)这样得到的是含list和其他结构的list,利用lapply把非list转化为list就可以了,事情的框架基本就是这两步,其余的代码干的事情是,光靠上面两步会出现一个a=4这种参数会被弄成f(...,4,)名字a会丢,原因就是在lapply完之后,名字并没有给大list的对应位置的等待合并的小list,那个循环就是用来给这样的名字的
通过这3个问题,我们都会发现基本上解决思路都是化为list(quote(fname),....)然后转成call的形式!
在继续深入call之前,我们来轻松一下,看下mode这个函数的实现
function (x)<br />
{<br />
if (is.expression(x))<br />
return("expression")<br />
if (is.call(x))<br />
return(switch(deparse(x[[1L]])[1L], <code>(</code> = "(", "call"))<br />
if (is.name(x))<br />
"name"<br />
else switch(tx <- typeof(x), double = , integer = "numeric",<br />
closure = , builtin = , special = "function", tx)<br />
}<br />
<bytecode: 0x082049d8><br />
<environment: namespace:base>
class和typeof的关系前面的帖子已经说清楚了,当时提到了mode,现在正式面对这个问题了,我们这里的概念,name,typeof出来是symbol,call,typeof出来是language,不过不用管了,看以看到mode是基于is.call,is.name来判断的,包括expression,然后就是switch(typeof(x),....)如果是double,integer返回numeric,如果是closure,builtin,special返回function,其它就是不动的返回
这里的closure就是我们写的函数,builtin是primitive函数,special是不计算参数的函数,它们都会返回"function"
typeof(expression)<br />
typeof(quote)
这样的mode有的时候对于初学者很有用,试想一开始typeof(x)的时候就出现closure,builtin,special,谁能想到它们都是function呢?好了,关于这方面知识,知道到这里暂时够了,以后深入再说吧[s:11]
好,下面来继续说call,我们要讨论的是捕捉当前的call.也就是在函数内部捕捉调用这个函数的表达式:
f <- function(abc = 1, def = 2, ghi = 3) {<br />
list(sys = sys.call(), match = match.call())<br />
}<br />
f(d = 2, 2)<br />
#> $sys<br />
#> f(d = 2, 2)<br />
#><br />
#> $match<br />
#> f(abc = 2, def = 2)
match.call()经常用于建模的函数,用于更新模型:
mod <- lm(mpg ~ wt, data = mtcars)<br />
update(mod, formula = . ~ . + cyl)<br />
#><br />
#> Call:<br />
#> lm(formula = mpg ~ wt + cyl, data = mtcars)<br />
#><br />
#> Coefficients:<br />
#> (Intercept) wt cyl<br />
#> 39.69 -3.19 -1.51
update就用来更新了模型,这是怎么做到的?我们来写出来
<br />
update_call <- function (object, formula., ...) {<br />
call <- object$call</p>
<p> # Use update.formula to deal with formulas like . ~ .<br />
if (!missing(formula.)) {<br />
call$formula <- update.formula(formula(object), formula.)<br />
}</p>
<p> modify_call(call, dots(...))<br />
}<br />
update_model <- function(object, formula., ...) {<br />
call <- update_call(object, formula., ...)<br />
eval(call, parent.frame())<br />
}<br />
update_model(mod, formula = . ~ . + cyl)<br />
#><br />
#> Call:<br />
#> lm(formula = mpg ~ wt + cyl, data = mtcars)<br />
#><br />
#> Coefficients:<br />
#> (Intercept) wt cyl<br />
#> 39.69 -3.19 -1.51
其实它的本质就是提取object的call,然后利用call的性质,用call$formula去修改formula这个参数,其中又利用了update.formula,这个函数是用C写的,作者一道练习是让我们用R写出来,我只能说,天啊,我还不了解formula这个类型,怎么能写[s:12]?
call的formula参数更新好后,就需要更新来自...的参数了,这个时候我们需要用到dots()来把...转成输入内容的list,然后又调用modify_call去修改,基于篇幅(呵呵[s:11]已经相当长了)我就不列出这两个函数的源码了,大家可以自己pryr::dots,主要的思路就是用dots获得...的输入内容的list,然后在modify_call里面,然后取出list的名字,在call本身的参数名字里写个循环,如果list的名字出现就修改call,用基本的$<-修改,modify_call还是值得一读的
这样一来的话,新的call的包括主要的formula在内的所有参数都被更新了,然后在update_model里eval一下就好了
最后值得注意一下的是,env的问题,我们自己的是在parent.frame里执行,而R原来的版本update会在globalenv()中执行,所以对下面的例子:
f <- function() {<br />
n <- 3<br />
lm(mpg ~ poly(wt, n), data = mtcars)<br />
}<br />
mod <- f()<br />
update(mod, data = mtcars)<br />
#> Error: object 'n' not found<br />
update_model <- function(object, formula., ...) {<br />
call <- update_call(object, formula., ...)<br />
eval(call, environment(formula(object)))<br />
}
就好了,本来的update是找不到n的,因为n在f的exe e中,而environment(formula())就记录了这个环境,formula我觉得后面会遇到,到时候会详细介绍的
3.pairlists
前面说过,pairlists是R过去的遗产,目前只保留在函数参数里,它们的表现和list很像,但底层是用链表而不是向量,原话如下:
Pairlists are a holdover from R’s past. They behave identically to lists, but have a different internal representation (as a linked list rather than a vector). Pairlists have been replaced by lists everywhere except in function arguments.
然后下面我们要做非常有趣的事情,创造函数,主要利用"function",给它参数,函数体然后在env,eval就可以了
make_function <- function(args, body, env = parent.frame()) {<br />
args <- as.pairlist(args)</p>
<p> eval(call("function", args, body), env)<br />
}
通常我们给的参数是alist()它不计算它的参数,alist(x=a)是list(x=quote(a))的等效简写形式,配合alist我们
add <- make_function(alist(a = 1, b = 2), quote(a + b))<br />
add(1)<br />
#> [1] 3<br />
add(1, 2)<br />
#> [1] 3</p>
<p># To have an argument with no default, you need an explicit =<br />
make_function(alist(a = , b = a), quote(a + b))<br />
#> function (a, b = a)<br />
#> a + b<br />
# To take <code>...</code> as an argument put it on the LHS of =<br />
make_function(alist(a = , b = , ... =), quote(a + b))<br />
#> function (a, b, ...)<br />
#> a + b
可以看到传特定参数的时候要注意的都写在上面了,它在闭包的应用也很方便,因为它可以更好的看函数体
adder <- function(x) {<br />
make_function(alist(y =), substitute({x + y}), parent.frame())<br />
}<br />
adder(10)<br />
#> function (y)<br />
#> {<br />
#> 10 + y<br />
#> }
这里稍微提下alist(a)和alist(a=)是有区别的,一个是某个位置是参数的值为a的name,一个是名字为a的参数的值为emptyname.
我们来看下make_function的应用之一partial函数,大家可以pryr::partial,这里不复制了,仔细阅读完源码,我们发现,它做的事情是得到一个指定参数以及加入...的call表达式,然后创建一个函数,参数是...,源码中用的是
list(...=expr=),我觉得也可以用我们这里提到的alist(...=),然后函数体就是那个call表达式,在加入环境以及一些细节的控制参数,一个partical就得到了,这个函数的思路非常值得学习,下面再看下unenclose,它的作用是在函数的ee环境中找到函数定义需要的值,它的源码我也不贴了,大家pryr::unenclose就可以了,在读这个函数之前,需要回忆下substitute_q的作用,它的作用就是传入的是quote,然后里面用双层substitute来产生结果,大家可以看下这个函数的源码,回到unenclose,其实就简单了,利用environment(f)返回f的ee,然后利用formals(f)返回参数,然后substitute_q(body(f),env)就得到替换值过的函数了,这个源码也值得推荐
最后我们来看下make_function的一个应用
curve(sin(exp(4 * x)), n = 1000)
这里的x是一个pronoun,这种函数叫anaphoric,在其他语言,Perl之类有应用.我们来看实现:
curve2 <- function(expr, xlim = c(0, 1), n = 100, env = parent.frame()) {<br />
f <- make_function(alist(x = ), substitute(expr), env)</p>
<p> x <- seq(xlim[1], xlim[2], length = n)<br />
y <- f(x)</p>
<p> plot(x, y, type = "l", ylab = deparse(substitute(expr)))<br />
}
其实很简单就是利用make_function,和x,以及函数表达式创造了一个函数,作者还列出了一种方法,利用substitute加env来找x的值
curve3 <- function(expr, xlim = c(0, 1), n = 100,<br />
env = parent.frame()) {<br />
env2 <- new.env(parent = env)<br />
env2$x <- seq(xlim[1], xlim[2], length = n)</p>
<p> y <- eval(substitute(expr), env2)<br />
plot(env2$x, y, type = "l", ylab = deparse(substitute(expr)))<br />
}
我们前面提到过deparse的作用,parse则相反的功能,它把字符串变成expression
exp <- parse(text = c("<br />
x <- 4<br />
x<br />
5<br />
"))<br />
length(exp)<br />
#> [1] 3<br />
typeof(exp)<br />
#> [1] "expression"</p>
<p>exp[[1]]<br />
#> x <- 4<br />
exp[[2]]
OK,paste返回一个expression里面包含各种具体的expression类型call,name,...
能把字符串处理成这样,那显然我们想到了source,从字符串文件得到并运行源码,我们来实现自己的source
<br />
simple_source <- function(file, envir = new.env()) {<br />
stopifnot(file.exists(file))<br />
stopifnot(is.environment(envir))</p>
<p> lines <- readLines(file, warn = FALSE)<br />
exprs <- parse(text = lines)</p>
<p> n <- length(exprs)<br />
if (n == 0L) return(invisible())</p>
<p> for (i in seq_len(n - 1)) {<br />
eval(exprs[i], envir)<br />
}<br />
invisible(eval(exprs[n], envir))<br />
}<br />
这段代码最主要的就是exprs<-parse(text=lines),然后遍历exprs,在一个新环境中eval它们,最后的隐式返回大家可以不必太在意
最后的最后,我们来介绍遍历AST的内容:
基本的都介绍了,我们可以用substitute,modify_call之类的函数以及相应的知识来修改call,但是如果要更复杂的功能,我们必须学着遍历ast,codetools包里有个findGlobals()可以寻找一个函数的依赖情况
f<-function(x=a,y=2){<br />
x+y<br />
}<br />
codetools::findGlobals(f)
对于我们要做的遍历ast树,基本上框架如下
recurse_call <- function(x) {<br />
if (is.atomic(x)) {<br />
# Return a value<br />
} else if (is.name(x)) {<br />
# Return a value<br />
} else if (is.call(x)) {<br />
# Call recurse_call recursively<br />
} else if (is.pairlist(x)) {<br />
# Call recurse_call recursively<br />
} else {<br />
# User supplied incorrect input<br />
stop("Don't know how to handle type ", typeof(x),<br />
call. = FALSE)<br />
}<br />
}
这部分挺难的,我们来仔细理解,从简单的事情做,首先第一个任务是寻找一个expression中是否存在T,F之类的逻辑简写
ast(TRUE)<br />
ast(T)
通过这个观察发现,如果是name有可能,如果是atomic不可能含有T,F所以代码如下
logical_abbr <- function(x) {<br />
if (is.atomic(x)) {<br />
FALSE<br />
} else if (is.name(x)) {<br />
identical(x, quote(T)) || identical(x, quote(F))<br />
} else if (is.call(x) || is.pairlist(x)) {<br />
for (i in seq_along(x)) {<br />
if (logical_abbr(x[[i]])) return(TRUE)<br />
}<br />
} else {<br />
stop("Don't know how to handle type ", typeof(x),<br />
call. = FALSE)<br />
}<br />
}<br />
logical_abbr(quote(mean(x, na.rm = T)))<br />
#> [1] TRUE
看上去挺简单,但是要真的理解我觉得还是蛮难的,关键在于x是call,pairlist那里的循环递归的理解,循环第一层次,遇到一个call深入遍历进去,一有T,F就返回,没有就继续第一层,如果深入一个call的时候又遇到一个call就又深入,然后遍历玩这个call,返回上个call的其余部分遍历,没有就跳出call的遍历,继续第一层遍历,一次类推。。。你被绕晕了没。。。[s:11]
第二个任务是找出所有被<-创建的变量,作者修改了这个函数好几次,每次带入一个问题,不断解决得到最后的版本,大家可以去原书看,太长了就不贴了
大家可以对着原书,看我的思路,思路是:第一遍当是call的时候判断name是不是'<-'是就返回call第2个参数,否则就继续遍历这个call,这样做的问题是输出结果不漂亮,于是第二遍都返回character,这样的结果会出现重复的名字,以及<-含有<-的情况就没有被考虑,于是第三遍用了unique,以及把继续遍历call的结构放到判断<-的外面,就是即使是<-也要遍历它,这些遍历可以进行的关键是,lapply一个call的时候,第一个已经是name了不会是call了所以不会死循环(我开始读的时候就纠结这里),最后针对names(a)<-1会出现问题,作者使用is.name()来确保返回的不是一个call的函数名
很复杂吧,慢慢熬吧[s:11]
作者还利用上面的框架实现了自己的bquote2,基本上就是is.call为TRUE的时候看是不是.()函数是的话eval第2个参数,不是就继续递归a<-lapply(x,bquote2,env)并要as.call(a),对于pairlist则直接递归,具体的大家自己去研究吧
PS:当我做这个练习的时候发现了作者的一个BUG
f<-function(x=TRUE) {<br />
+ g(x+T)<br />
+ }<br />
#try let this work<br />
logical_abbr(f)
我的解决方法
logical_abbr(call("function",formals(f),body(f)))
本来应该是对的,结果老错,我就以为是我自己写错了,结果后来发现竟然是作者些的logical_abbr函数有问题!
logical_abbr <- function(x) {<br />
if (is.atomic(x)) {<br />
FALSE<br />
} else if (is.name(x)) {<br />
identical(x, quote(T)) || identical(x, quote(F))<br />
} else if (is.call(x) || is.pairlist(x)) {<br />
for (i in seq_along(x)) {<br />
if (logical_abbr(x[[i]])) return(TRUE)<br />
}<br />
FALSE #应该在这里加个FALSE !!<br />
} else {<br />
stop("Don't know how to handle type ", typeof(x),<br />
call. = FALSE)<br />
}<br />
}
</p>