2012年11月17日星期六

【R Special】 函数式语言的面向对象机制实现


【R Special】 函数式语言的面向对象机制实现

好久没更新博客了,一方面因为工作里挖了太多的坑,要一个一个去填,另一方面也是因为这阵子把今年整年的旅行都给规划了一遍。然后突然心血来潮,要 给R写一个系列的文章,有部队的编排,感觉会比以前那样打游击要好。于己可以更有体系的总结过往实践过的知识,于他人,也方便特意或偶然来到这个博客的读 者查阅。

前言

翻过好些R的书籍,都是从入门到精通型,三章之内完全体现不出R的特点,更让人找不到理由为什么还要去学习这第k+1门语言。这样general的 介绍方式倒也情有可原,你不从基本数据结构和基本流程控制讲起又怎能唤起入门者的强烈共鸣呢。R的几篇官方文档倒是值得一看再看,但枯燥无味基本不属于普 通人类的阅读范畴。于是就想写一些简明易懂的R的special方面的东西,这个尝试会比较随性,想到哪写哪,哪天要是没有动力,说不定就烂尾了。
意想中这不是个入门系列,而是我个人偏好摘取的R的一些Feature汇集,具有一定程度的进阶及挑战性,甚至是编程思维和对问题建模方式层面的特点,也是R之所以为R的原因,所以定名为R Special。
粗略归纳自己在实践中思考过的或深或浅的内容,预计至少会有:R的基本数据类型及底层数据结构、向量化思维及实现、矩阵(密、疏)运算、函数式编程 特性、各种常用的统计分析、统一的分类模型、并行计算、如何用R来描述算法等等内容,第一篇就介绍函数式语言的面向对象机制可能不太合适,但正好这些天又 看回这部分内容,所以,无所谓了。

面向对象的函数式语言

以前我介绍R的六张面孔时 曾说过,R是面向对象的函数式语言,这是他特有的两张面孔。一般来说,函数式和面向对象这两个特性很难并存于同一种语言中。基于它lisp和 fp(function programming)的基因,R在融合这两个稍有对立的特性时其实更多地是体现函数式的一面,再通过一些附加的机制实现了面向对象中的继承及多态的特 性,这种实现只重实质不重形式,所以不要惊呼:“我熟悉的Class关键词呢”?接受它有助于你理解数学家们是怎么抽象地去解决一个工程问题的。
R的面向对象一方面体现在其内置的数据结构都表现为类这个概念。对于R,万物皆对象,类是对象的抽象描述,对象是类的实例化。R有一个class()函数,你可以给它传入任何变量(任何!包括这个函数本身),它会返回描述这个对象的类的名字。
R的面向对象另一方面体现在你可以利用它提供的面向对象的编码方式来描述自己的算法,组织自己的代码。为达到这个目的,R采用的并非如C++、 Java那样的层次型的类结构,而是利用了泛型函数(generic function)这个概念。泛型函数的OOP并不关心特定的语法结构,而专注于OOP最主要的问题——代码分派(dispatch),即:“调用什么代 码”以及“如何做出这一决策”。常见的OOP语言通过内化的继承及重载的方式来完成这一使命,重心在class;而R则通过定义一系列泛型函数,以及这些 函数在不同类型对象上的行为来达到这个目的,重心在method。前者结构严谨,但未免冗余笨重,系统越大缺陷越明显;后者抽象,但因其关注在函数本身而 成为与fp最完美的结合方式。

S3与S4的OOP系统

先看如下两段代码及其注释。
S3系统
1
2
3
4
5
6
7
8
9
10
11
12
> print   # 显示print这个函数的代码
 function (x, ...)
 UseMethod("print")  # 调用UseMethod这个函数相当于隐式地定义了print为泛型函数,同时这个函数担当了代码分派决策的角色
 
 > x <- c(1,2,3) > class(x)  # 任何对象都可以通过这个方法得到其class[1] "numeric"
 > print(x)  # print函数对numeric这个class的默认行为
 [1] 1 2 3
 > class(x) <- 'num'  # 可以随意修改某个对象的class名字 > class(x)
 [1] "num"
 > print.num <- function(x) {cat('this is my numeric:', x, '\n')} # 为num这个新classprint对应的method > print(x)  # 你猜到了,泛型函数的分派策略是寻找'print.%s'%class(x)这个函数(这里是python风格的string format)
 this is my numeric: 1 2 3
S4系统
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 定义一个类的名字及其成员变量(R中成为slot,通过@操作符选择)
 setClass('player', representation(name='character'))
 # contain关键词表示继承关系,prototype相当于初始化
 setClass('superstar', representation(club='character'),
 prototype=list(club='none', name='anonymous'), contain='player')
 # 因为print已经是一个generic函数,所以可以直接绑定method到某个类,否则需要先setGeneric才能setMethod
 setMethod('print', 'superstar',
 function(x, ...){
 cat(sprintf('%s is from %s\n', x@name, x@club))})
 # 通过new来生成类的一个实例
 ronaldo = new('superstar', name='ronaldo', club='RM')
 messi = new('superstar', name='messi', club='Barca')
 # 根据setMethod的绑定关系来分派
 print(ronaldo)
 print(messi)
 # 以上代码另存为一个文本文件,如object.R
 
> source('object.R')
 ronaldo is from RM
 messi is from Barca
可以看到,R提供了S3和S4这两种不同的机制来实现OOP编程,后起的S4兼容S3。
S3是R中最古老的class/method系统,绝大部分R自带的方法都是基于S3的泛型函数,如print, summary, plot。在这套系统里,class基本处于一个无足轻重的地位,因为你可以给任意对象指定任意一个名字来指代它的class,就如列表1的第10行所示 的那样。在这里,class只是一个名字而已,比人类起名字还随便。它不必有具体的数据结构、具体的事物含义,也无需声明、初始化、析构,更别说严谨的继 承重载结构,这对当初只看过C++和Java的OOP的我来说无疑是一枚深水炸弹啊。但这个名字所起的唯一作用也是最重要的作用——代码分派!
虽然已经完成任务,但S3仍略显随意和简陋,并非一个完全的面向对象的系统,所以后来又发展出一个完全平行的S4版本的class/method系 统来。因为R是个完全基于package的环境,所以所谓“自带”的S3系统其实来自于base这个基本包,而S4来自于methods这个包,但跟 base一样,这个包在R启动时会默认被装载,所以也可以认为是自带的。S4时代class的地位相对于S3有了很大的提升,可以通过setClass来 声明一个class,定义它的数据结构,以及进行完整性检验。泛型函数的声明也不再是S3那样的隐式实现,而是通过setGeneric来显式声明。 setMethod函数则实现了前两者的绑定。
你可以按自己的喜好选择任一个系统来完成你的OOP建模,在功能上它们不会有任何差别,互不干扰。官方建议新的项目应该使用更为严谨的S4系统,但很多人都还是喜欢用S3,理由是它够”quick and dirty”【2】。

使用R的OOP建模方式

无论是使用简单的S3还是更为规范化的S4,基于其泛型编程的侧重,在R环境下对你所面临的问题进行面向对象式的建模跟你以往的习惯会有所不同。首 先需要把你的问题中的“操作”抽象成已有的或现写的泛型函数,这就是generic function;然后定义你的问题中涉及的对象及其数据结构,给它们一个名字,这就是class;最后为每个需要泛型操作的对象实现一个方法,这就是 method(method即是泛型函数在某个class的具体实现)。

为什么要引入面向对象

最初我也疑惑,像R这种fp和统计混血的小孩为什么要穿上一身市面上流行的”面向对象“的外衣,以致于你在R里使用的所有自带的函数都实际上都是泛型函数,包括运算符。这里我不打算废话了,通过两个例子来说明这个机制所带来的好处。
常用的输出一般性统计量的函数summary是泛型函数
> summary(1:100) # 一个向量的一般统计量
Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
1.00   25.75   50.50   50.50   75.25  100.00
> summary(matrix(1:100, nrow=50))  # 一个矩阵的一般统计量(其实是按列计算)
V1              V2
Min.   : 1.00   Min.   : 51.00
1st Qu.:13.25   1st Qu.: 63.25
Median :25.50   Median : 75.50
Mean   :25.50   Mean   : 75.50
3rd Qu.:37.75   3rd Qu.: 87.75
Max.   :50.00   Max.   :100.00
> fit=lm(y~x, data.frame(x=1:100, y=1:100+rnorm(100)))
> summary(fit)
# 输出为这个线性模型的一系列拟合结果及参数,此处略。
#高级一点,分类器模型常用的predict函数也是泛型函数。
> predict
predict       predict.glm   predict.lm    predict.mlm   predict.poly
以上几个对predict进行了扩展的method可以保证lm, glm, mlm, poly这几个不同的分类器model的返回结果都可以通过一个统一的predict函数进行预测——多么优雅的抽象。
最后,不要赶潮流随便地就给你的项目引入面向对象,这不是值得夸耀的feature的一部分,而应该是让的你的feature更好地被实现的工具。 R引入了这个机制,使得不同的对象都可以使用一致的方法(数学家们这个时候肯定会洋洋得意地认为自己抽象出了一个算子——还记得算子吗亲),整个算法的描 述变得抽象、简洁又优雅,这才是最重要的。
【参考资料】
【1】使用 R 编写统计程序,第 3 部分: 可重用和面向对象编程: http://www.ibm.com/developerworks/cn/linux/l-r3.html
【2】Classes and Methods in R: http://www.biostat.jhsph.edu/~rpeng/docs/R-classes-scope.pdf
关于作者

没有评论:

发表评论