5.1 抽象化
R是一个好东西的主要原因是它是一门语言.而语言的魅力便是抽象.在R语言中,函数便是实现抽象的方式.假如我们想要把1到3的整数重复两次.这是一个简单的命令:
c(1:3, 1:3)
现在假如想要重复这些数字六次或者六十次.用函数来抽象这个操作就变得有意义了.事实上,这种抽象已经有前人做了:
rep(1:3, 6)
rep()可以完成我们上面的任务和其他一些相似的任务.
我们在做一个新的任务.我们有两个向量;我们想生成一个新的向量,首先将第一个向量重复到第二个向量的长度,然后第二个向量重复到第一个向量的长度.一个向量被重复到的长度如果小于它本身的长度意味着只需要这个向量的前边的那一部分.使用rep()函数可以轻松地将其抽象到一个函数中:
repeat.xy <- function(x, y)
{
c(rep(x, length=length(y)), rep(y, length=length(x)))
}
repeat.xy()现在就可以在R中被使用了.
repeat.xy(1:4, 6:16)
这种很轻易地写出一个函数好像就意味着我们能够很自然地从仅仅使用R升级到用R编程.
除了可以抽象操作,函数还凝结着智慧.π大约是 3.1415926535897932384626433832795028841971693993751058209749445
923078便是智慧.
函数:
circle.area <- function(r) pi * r ^ 2
智慧与抽象兼而有之–它可以帮你算出任何你想知道的园的面积(约等于).
这里并不是一个纯粹讨论R语言结构的地方,而是一个评论我们上边讲的两个函数细节的地方.repeat.xy()的主体被一对大括号包围着而circle.area()没有.函数的主体应该是一个简单地表达式.大括号将主体内的表达式转换成一个单独的(组合)表达式.当函数的主体只有一条命令的时候,大括号是可选的.大括号也被应用在循环,分支和判断的结构中.
理想的情况是每个函数通过让人容易理解的输入输出完成一个被明确定义的任务.初学者通常都是用一个函数来完成所有的事情.一个普遍来讲总是更好的方法是:写很多的小巧的函数,然后有一个更大的函数调用这些小的函数来完成所有事情.一步步将任务拆分可以让我们清楚地知道什么是真正应该去做的.而且当程序出现bug时可以让我们的调试变得更简单.使用小函数普及程度更高.
R语言抽象的一个瑰宝是函数入参的默认值.比如,sd()中的入参na.rm的默认值是FALSE.如果你的需求正好是FALSE,你在调用sd()的时候便不需要修改na.rm的值.如果你希望丢掉缺失值,你在调用sd()的时候加入参数na.rm=TRUE.假如你编写了自己的函数而仅仅只是改变函数默认入参,那么你可能就不会感激函数提供给你的抽象性了.
函数最总返回一个结果给你,而函数的返回值几乎证明着自己的存在.函数中最后的一条语句被定义为返回值.然而很多函数并没有服从这样的机制,但是return()会强制返回你想返回的.
函数的其他一些影响是它会有一个或者多个负作用.一个负作用便是除了会返回结果外,还会改变代码系统.R的哲学是将这些负作用集中在少数几个我们明确知道的并且希望对系统造成影响的函数中(比如print(),plot(),rm()).
R函数处理的事物是对象.R有丰富的对象类型.表 5.1展示了一些重要的对象类型.
你也许会注意到每个原子类型都有一个可能存在的值–NA(Not Available),这被称作缺失值,一些初次接触R的使用者花费了大量的时间努力去避免NAs.对于0的首次出现,他们或许也会这样做.然而NA是一个对你非常有价值的好东西.当你的数据中存在缺失值的时候你通常是不会高兴的,但是生活有NA总比没有好.
R的设计理想是nothing is important.我们再读一次”nothing” is important.向量的长度可以为0.这是另外一个愚蠢的设计但是被证明是难以置信地有用–这么说来并不是愚蠢的设计啦.我们并不经常去处理一个不存在的事物,所以一些情况下,这是个问题–我们将会在轮回8,轮回8.1.15看到示例.
对象的大多数价值在于处理它们的属性.很多属性改变着R和使用者对它们的认知.通常大多数情况下,对象的一个属性都会有自己的名字.决定着对象的方向的属性是类别.表5.2列举了少许非常重要的由属性决定的对象.
初学者的一个普遍问题是把数据框和矩阵混为一谈.它们看起来好像一样.但是它们确实是不一样的.在轮回8.2.37将会为你展示它们为什么不同.
“vector”在R中有很多含义:
1. 一个原子对象(和列表相反),这或许是最普遍的用法.
2. 一个没有属性的对象(除了可能名称).这是被is.vector()和as.vector()影响的定义.
3. 一个可以有任意长度的对象(包括列表).
很明显第一个定义和第三个定义看起来是矛盾的,它们的到底属于哪一个只有通过代码的上下文才能够清楚.当我们讨论向量跟矩阵的区别时,第二个定义就派上用场了.
单词”list”在R中有一个专业的含义–一个可以包含不同类型,包括自己本身的长度可伸缩的对象.有时这个单词也会被用到非专业的地方,比如在”search list”或者”argument list”.
并不是所有的函数都被平等地被制造出来.它们可以被随意地分为三种类型.
下边是一个无名函数:
apply(x, 2, function(z) mean(z[z > 0]))
这个函数充当了apply()的第三个参数,它是如此短暂以至于我们都没必要给它命名.
这些函数仅仅在一个特定的场合被使用,这些就是你的一次性函数.
然而对于一些对你来说确实珍贵的函数.本来它们就是一次性的而你却重写它们让它们变得更加抽象.诚然,你可能非常希望一个文件或者R包能够引入你的珍贵有用的函数.
从无名函数的例子中我们可以看到,一个函数也可以是另外一个函数的入参.在R中,函数和向量或者矩阵一样都是对象.你可以把函数想象成数据.
一个全新级别的抽象是一个函数的返回值是另外一个函数. 经验分布函数就是一个例子:
> mycumfun <- ecdf(rnorm(10))
> mycumfun(0)
[1] 0.4
只要你写了一个函数返回另一个函数的这种代码,你就可以直接去下一个轮回了.
在第二轮回(12页)我们短暂地分析了do.call().一些人对这个函数甚是迷惑.这是没有必要且不幸的–实际上它是一个非常简单但又非常强大的函数.一般情况下我们都是通过函数的的函数名外和一个”入参列表”来调用这个函数,这样很简单:
sample(x=10, size=5)
do.call()函数允许你以一个真正的列表来提供函数的入参:
do.call("sample", list(x=10, size=5))
有时在调用一个函数时能看到具体的执行情况是非常有用的.函数被调用的时候会建立一个环境,当这个被调用的函数再去调用其他函数时,系统也会为这些其他函数建立自己的环境.因此内存里会有一个环境栈随程序的运行而伸缩.
让我们定义一些函数吧:
ftop <- function(x)
{
# time 1
x1 <- f1(x)
# time 5
ans.top <- f2(x1)
# time 9
ans.top
}
f1 <- function(x)
{
# time 2
ans1 <- f1.1(x)
# time 4
ans1
}
f2 <- function(x)
{
# time 6
ans2 <- f2.1(x)
# time 8
ans2
}
然后我们进行调用:
# time 0
ftop(myx)
# time 10
图 5.1向我们展示了这次调用随着时间栈中的环境怎么变化的.注意在ftop(),f1(),f2()的环境中都有一个x.x在ftop()中被称作myx(或者可能是x的副本),在f1()也是这样.但是f2()中的x就有些不同了.
当我们调试代码的时候,我们将会研究这个栈特定时刻的情况.比如,如果一个代码当一个bug出现在f2.1,我们就要查找在time 7附近的栈的情形.
R语言有丰富的对象类型.这是R优点的一部分.这些对象的一些是语言本身的元素–函数调用,表达式等等.这提供了一种非常强大的抽象形式–在语言上计算.虽然几乎所有的新人对语言的元素混淆看起来是十分的令人费解的,然而很多人对这种观点十分迟钝.