Rのオブジェクト指向システム(S3/S4/RC)
総称関数を実現するための仕組みに過ぎない感があるRのオブジェクト指向プログラミングですが、S3/S4の総称関数はとても便利な仕組みですし、S4クラスのシステムはもう少し高度な機能を提供しています。また、RCは他のプログラミング言語に近い感覚で利用することができます。
Rではありきたりの統計解析にRを用いている限りにおいて、オブジェクト指向プログラミングを意識することはないです1。総称関数は便利に使われているのですが、JavaやC++と言った所謂クラス型オブジェクト指向言語でするようにGoFデザインパターンでガリガリ書くのには無理があるからでしょう2。しかし、上手く使うとコードの見通しをよくできるので、不完全3でも有用な機能にはなっています。
Rのオブジェクト指向システムは歴史的な理由でS3/S4/RC/R6と4種類もあるのですが、標準のS3とS4とRC(Reference Classes)を確認していきましょう。
1 S3クラスによるオブジェクト指向
もっとも素朴なS3クラスは特に労力がかかりません。リストにクラス名の属性がついて、総称関数が使えるようになっただけです。インスタンスと言う概念すらありません。
1.1 S3クラスを作ってみる
空のlist
にクラス名を指定してもS3クラスは完成するのですが、後のことを考えてもう少し手の込んだオブジェクトをつくります。
$a
[1] 1 2 3 4 5 6 7
$b
[1] 2 1 6 4 4 8 6
attr(,"class")
[1] "bigger one lover"
1.2 既存の総称関数のメソッドを作る
このリスト構造にはmax
関数が使えません。
max(obj1) でエラー: 引数 'type' (list) が不正です
“bigger one
lover”クラスのために、総称関数max
の実装を用意しましょう。
総称関数にあわせて第2引数が必要になっていることと、max(..., na.rm)
は既にgeneric関数になっているためUseMethod不要なことに注意してください。
[1] 7 8
max
が使えるようになりました。
1.3 新たな総称関数をつくる
新しい総称関数をつくるのも簡単です。
choose <- function(obj) UseMethod("choose")
"choose.bigger one lover" <- function(obj){
pmax(obj1$a, obj1$b)
}
これでリスト内のベクターaとbの順番が対応する要素を比較して大きい方を戻すメソッドとなります。定義した関数choose
が振り分けを行なうUseMethod
を呼び、UseMethod
がobj
のクラス名に応じて4メソッドを実装した関数を呼ぶ仕組みです。作ったchoose
を消せば、総称関数では無くなります。
[1] 2 2 6 4 5 8 7
演算子も定義できます。
"%+%" <- function(v, w) UseMethod("%+%")
"%+%.bigger one lover" <- function(v, w){
c <- list(a=v$a + w$a, b=v$b + w$b)
class(c) <- class(v)
c
}
obj2 <- list(a=(-1)^(1:n), b=(-1)^(2:(n+1)))
class(obj2) <- c("bigger one lover")
obj1 %+% obj2
$a
[1] 0 3 2 5 4 7 6
$b
[1] 3 0 7 3 5 7 7
attr(,"class")
[1] "bigger one lover"
複数の引数を持つ場合はUseMethod
を呼んでいる関数の引数が複数になることに注意してください。メソッドが多様で総称関数を明確に規定できない場合は、...
を入れておきましょう。
1.4 S3クラスの継承と言うか親の設定
obj1
をobj3
にコピーして、obj3
のクラス名smaller one lover
を書き替えて、メソッドを定義します。
obj3 <- obj1
class(obj3) <- c("smaller one lover")
"choose.smaller one lover" <- function(obj){
pmin(obj1$a, obj1$b)
}
すると、同じ総称関数でも結果が変わります。
[1] 2 2 6 4 5 8 7
[1] 1 1 3 4 4 6 6
また、smaller one lover
クラスで実装されていないmax
はobj3
には使えません
max(obj3) でエラー: 引数 'type' (list) が不正です
親クラスを追加すると、実装されていないメソッドは、親のメソッドを呼ぶようになります。
[1] 7 8
Java™の派生クラスは実子、RのS3クラスの派生は養子感がありますね。
2 S4クラスによるオブジェクト指向
Rには、もう少しクラス定義を厳密にしたformalなクラスと呼ばれるS4クラスもあります。クラス定義とインスタンスの生成が明確にサポートされます5。
2.1 S4クラスの宣言
前節のS3クラスの作成をS4クラスに書きなおすと、
n <- 7
BOL <- setClass("bigger one lover",
representation(a = "numeric", b = "numeric"),
prototype(a = rep(0, n), b = rep(0, n)),
validity = function(object){
if(length(object@a)==length(object@b)) return(TRUE);
"a and b are different lengths!";
})
とrepresentation
による型宣言、prototype
による省略値、validity
による整合性チェックが設定できます。
継承もcontaines
で出来ます。
オブジェクトのメンバーはgetSlots
で確認できます。
a b
"numeric" "numeric"
なお定義に関する情報は環境内に特殊な変数名で保存されています6。またsetClass
の戻り値はコンストラクタになる関数です。
定義したクラスを削除する場合は、以下のようにしますがドキュメントには非推奨とされています7。
2.2 S4クラスからのインスタンス生成
実際にインスタンスを生成してみましょう。
コンストラクタを使うこともできます。
相変わらず実際はリストですが、(slot
もしくはattribute
と呼ばれる)メンバー変数になる要素を参照するときは、$
ではなく@
を用いることが推奨されています。
一応、中身が見られることを確認します。
[1] 1 2 3 4 5 6 7
[1] 6 5 7 2 4 4 4
obj1
とメンバー変数の値が同じの派生クラスもつくっておきます。
[1] 1 2 3 4 5 6 7
[1] 6 5 7 2 4 4 4
2.3 S4クラスのメソッド
S3クラスと同様に、総称関数を宣言しメソッドを実装します。
[1] "choose"
setMethod("choose",
signature(object = "bigger one lover"), function(object) {
pmax(object@a, object@b)
}
)
setMethod("choose",
signature(object = "smaller one lover"), function(object) {
pmin(object@a, object@b)
}
)
第1引数でメソッド名、第2引数でメソッドの引数の型、第3引数でメソッドの実態を定義します。第2引数は"bigger one lover"
とクラス名だけ書いておいても問題ないです。
メソッドの存在確認と、メソッド呼び出しもできます。
[1] TRUE
動かして確認すれば間に合いそうですが。
[1] 6 5 7 4 5 6 7
[1] 1 2 3 2 4 4 4
元クラスも派生クラスも動きますね。
定義したメソッドの削除もできます。
2.4 既存の総称関数のメソッドを作る
max
も同様に定義したいわけですが、既存の総称関数のメソッドを作るときには注意が必要です。登録されている総称関数の宣言に第2引数の変数名もあわせないと、エラーになるからです。最初に、総称関数max
の宣言を確認しましょう。
standardGeneric for "max" defined from package "base"
belonging to group(s): Summary
function (x, ..., na.rm = FALSE)
standardGeneric("max", .Primitive("max"))
<bytecode: 0x589964f7e158>
<environment: 0x5899651414c0>
Methods may be defined for arguments: x, na.rm
Use showMethods(max) for currently available ones.
x
とna.rm
を使っていますね。ここに注意して以下のように定義します。
setMethod("max",
signature(x="bigger one lover",
na.rm = "logical"),
function(x, na.rm) {
c(max(x@a), max(x@b))
}
)
...
は無視して無問題のようです。仕様上は...
も使えるはずですが8。
加算もgetGeneric("+")
で定義を確認して変数名e1
とe2
に注意して実装してみましょう。
setMethod("+",
c(e1 = "bigger one lover", e2 = "bigger one lover"),
function(e1, e2) {
new(class(e1)[1], a = e1@a + e2@a, b = e1@b + e2@b)
}
)
obj1 + obj3
An object of class "bigger one lover"
Slot "a":
[1] 2 4 6 8 10 12 14
Slot "b":
[1] 12 10 14 4 8 8 8
なお、multi-dispatchなので、第2引数をc(e1 = "bigger one lover", e2 = "numeric")
としたメソッド、c(e1 = numeric, e2 = "bigger one lover")
としたメソッドを定義し、インスタンスと数値の加算も実現できます9。
2.5 S3クラス/メソッドのS4クラス/メソッドの相互運用
クラス名が分かればよいので、@
でなくて$
でメンバー変数(S4用語ではslot)を参照することになりますが、S4クラスのインスタンスを処理するS3クラスのメソッドを作ることは簡単です。逆にS4クラスでS3クラスのオブジェクトを取り扱うこともできます。
S3クラスを作成してみます。
このままでは扱えないのですが、setOldClass
を使うとS4クラス用の書き方で処理できるようになります。
[1] "plus"
setMethod("plus", c("example", "example"), function(v, w) {
c <- list(a=v$a + w$a)
class(c) <- class(v)
c
})
plus(obj1, obj2)
$a
[1] 0 1 2 3 4
attr(,"class")
[1] "example"
ただし(よく確認できていないのですが)+
の実装が上手くいかなかったので、完璧な後方互換と言えるかは分かりません。
2.6 S4クラスの利点
S4クラスの方が優れている点としては、
- S3クラスよりは厳密な定義が可能
- 引数の型に応じてメソッドを変えられるmulti-dispatch
- メソッド名のsuffixにクラス名がつかない
+
や-
などの演算子も定義可能
ところが挙げられます。煩雑なのでad-hocなコードで使うのは億劫ですが、パッケージ化などをする場合は有用だと思います。
3 RC(Reference Classes)
利用頻度は低いらしいですが、Java™使いなどには自然に思えるのがRCです。
Rは関数の引数が原則として値渡しなので、メモリー利用効率と付随して処理速度が良くありません。S3とS4も同様の欠点があります。しかし環境(environment
)が例外的に参照渡しであることを利用して、RCはこの欠点を緩和しています。記述も他のオブジェクト指向型言語に近いものですし、ユーザー定義クラスはすべてenvRefClass
の派生になっているなど、クラスのつくりも似ています。
3.1 RCクラスの宣言/メソッド定義
オブジェクト指向らしくクラス宣言とメソッド定義が同時に済みます。
BOL <- setRefClass("bigger one lover RC",
fields = c(a = "numeric", b = "numeric"),
methods = c(
initialize = function(a = rep(0, n), b = rep(0, n)){
if(length(a)!=length(b)) stop("a and b are different lengths!");
.self$a <- a
.self$b <- b
},
max = function(.self){
c(max(.self$a), max(.self$b))
},
choose = function(.self){
c(pmax(.self$a, .self$b))
},
add = function(.self, obj2){
.self$a <- .self$a + obj2$a
.self$b <- .self$b + obj2$b
}
)
)
Java/C++/Python/PHPあたりに慣れている人だと説明不要な気がするのですが、bigger one lover RC
がクラス名、fields
がプロパティ、methods
がメソッドの定義です。initialize
が初期化メソッドで、各メソッドでは.sef
で自分を参照できます。
contains
で親クラスを指定し、プロパティやメソッドを上書きすることで継承もできます。初期化メソッドinitialize
も継承してくれます10。
SOL <- setRefClass("smaller one lover RC",
contains = "bigger one lover RC",
methods = c(
choose = function(.self){
c(pmin(.self$a, .self$b))
}
)
)
デフォルトで実装しているプロパティやメソッドは?setRefClass
で見られるドキュメントに網羅されているので、文書が散らばっていて全体構造が見えづらいS3/S4クラスより話が分かりやすいです。なお、演算子のオーバーライドのサポートは無いようです。Rの文法上実装不能なのだと思いますが、アクセス修飾子もありません。
3.2 RCクラスのインスタンス生成
インスタンスを作ります。
n <- 7
obj1 <- BOL(1:n, rpois(n, n*2/3))
obj2 <- SOL(1:n, rpois(n, n*2/3))
obj3 <- BOL(-1^(1:n), -1^(2:(n+1)))
中身もしっかり詰まっています。
Reference class object of class "bigger one lover RC"
Field "a":
[1] 1 2 3 4 5 6 7
Field "b":
[1] 2 5 4 5 5 4 3
Reference class object of class "smaller one lover RC"
Field "a":
[1] 1 2 3 4 5 6 7
Field "b":
[1] 2 3 9 8 10 4 3
Reference class object of class "bigger one lover RC"
Field "a":
[1] -1 -1 -1 -1 -1 -1 -1
Field "b":
[1] -1 -1 -1 -1 -1 -1 -1
4 まとめ
S3/S4/RCを俯瞰してきましたが、オブジェクト指向の部分だけとは言え3つのシステムが共存していて困惑する面があります。さらにRCより高速に動作するのが謳い文句のR6パッケージが存在します。どれを使えばいいのかと言う感じになりますが、仕様の自然さや簡素さと、オブジェクト指向の徹底度を天秤にかけて、個人利用ではS3、小規模チームではS4、大規模チームではR6と言うガイドラインを示している人がいます。Javaなどに慣れていれば、個人利用でもR6で問題ないとも思いますが。
lm
関数の戻り値をsummary
関数にいれたら詳細が出ると言うことは利用者のほぼ全員が知っているわけですが、summary
関数が総称関数で実態が別にあることを知らないか忘れてしまっている利用者は過半では無いでしょうか。↩︎標準APIを含めてライブラリがOOPで書かれていないと、それを利用するコードもOOPになりづらいです。RはAPIが非OOPでエンドユーザーのコードが非OOPなので、利用者がOOP好きでないとOOPを使う場面がやってきません。↩︎
アクセス修飾子が無いのでカプセル化が十分ではないし、Java™で言うinterface/abstrat classなども定義できないです。↩︎
親環境を辿って勝手に引数の内容を確認するのが気持ち悪いですね。↩︎
S3でもオブジェクト生成関数をつくれば似たような構造にはなるわけですが、クラスの構造を調べたりするシステム的なサポートはないので。↩︎
ls.str(all.names=TRUE)
でそれらしいprefixの変数を確認できます。↩︎setClass
でつくったクラスからインスタンスを生成後、クラスをremoveClass
で消して、同名のクラスをsetRefClass
でつくった後に、setRefClass
でつくった方のクラスからのインスタンス生成にエラーが出たり、生成されたインスタンスが異常になるので、しっかり機能していないかも知れません。↩︎? dotsMethods
で利用例が確認できる。↩︎numericやlogicalやcharacterもクラスとして扱われていると言うことです。↩︎
Java™のコンストラクタは継承されません。↩︎