R5クラスの継承の罠

あ...ありのまま 今 起こった事を(ry

R5クラスでサブクラスを定義すると、スーパークラスインスタンスされます。

な… 何を言っているのか(ry

本当に意味が分からないです。

setRefClass("Parent",
            methods=list(
              initialize = function()print("run")))
setRefClass("Child", contains = "Parent")

Parentクラスはインスタンス化されるさいに"run"と表示するだけのクラスです。ChildクラスはParentクラスを継承しているだけのクラスです。

> setRefClass("Parent",
+             methods=list(
+               initialize = function()print("run"))
+ )
> setRefClass("Child", contains = "Parent")
[1] "run"
[1] "run"

実行するとこうなります。Childクラスのクラス定義を行っただけで、なぜかParentクラスが二回インスタンス化されました。

この挙動は、以下のようなクラス定義の場合に問題を引き起こします。

setRefClass("Parent",
            fields = c("x", "y"),
            methods = list(
              initialize = function(...) {
                initFields(...)
                y <<- x * 2
              })
)
setRefClass("Child", contains = "Parent")

Parentクラスはインスタンス化されるさいにy <<- x * 2を実行します。なので、new("Parent", x=1)のように、インスタンス化のさいに引数でxの値を与えておかなければ、エラーになります。ChildクラスはあいかわらずParentクラスを継承しているだけのクラスです。

> setRefClass("Parent",
+             fields = c("x", "y"),
+             methods = list(
+               initialize = function(...) {
+                 initFields(...)
+                 y <<- x * 2
+               })
+ )
> setRefClass("Child", contains = "Parent")
 以下にエラー x * 2 :  二項演算子の引数が数値ではありません 

Childクラスはクラス定義すら行えません。

対応策としては、引数に初期値を与えておく、引数が与えられていない場合は即return()するかなんらかの例外処理を行う、などがあると思います。つまり、引数が与えられていなくても問題が起きないようにする、ということです。

個人的に一番問題だと思うのは、パッケージでR5クラスを使う場合です。パッケージをバイナリにビルドするときやlibrary()で読み込むときには、パッケージ内のスクリプトがいちど全部実行されるのですが、このときスクリプトのファイルが読み込まれる順番は辞書順です*1。サブクラスの前にスーパークラスが定義されていなければならないというのは、まあスクリプト言語では普通だと思います。しかし、R5クラスの場合、スーパークラスのinitializeメソッド内で使われている関数やクラスが、サブクラスの定義よりも前に定義されている必要がある、という複雑な問題が起きてしまいます。上記の対応策を取っておけば回避はできますが、知らなければ謎のバグに苦しむことになります(なりました)。

この副作用、どこかの文書に載ってるんでしょうか…?

> sessionInfo()
R version 3.0.0 (2013-04-03)
Platform: x86_64-w64-mingw32/x64 (64-bit)

locale:
[1] LC_COLLATE=Japanese_Japan.932  LC_CTYPE=Japanese_Japan.932   
[3] LC_MONETARY=Japanese_Japan.932 LC_NUMERIC=C                  
[5] LC_TIME=Japanese_Japan.932    

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods  
[7] base     

loaded via a namespace (and not attached):
[1] codetools_0.2-8 tools_3.0.0

*1:いろんなパッケージのソースを見てみると、a.Rとかzzz.Rという謎の名前のファイルがありますが、これはつまり最初に読み込んでほしいファイルと最後に読み込んでほしいファイルです。