この文書は,Winston Chang によるRパッケージ R6
(version 2.2.0) のビネット "Portable and non-portable R6 classes" の日本語訳です.
License: MIT
関連文書
Rの参照クラスにおける限界のひとつは,パッケージの名前空間をまたいだクラス継承が制限されていることです.R6では,portable
オプションを有効にするとこの問題が避けられます.
問題
参照クラスでのパッケージ間継承における問題の例を示します.pkgAにClassAがあり,pkgBにはClassAを継承したClassBがあるとします.ClassAには,pkgAからエクスポートされていない関数fun()
を呼び出すfoo()
メソッドがあるとします.
ClassBがfoo()
を継承すればfun()
を呼び出そうと試みますが,ClassBはpkgAではなくpkgBの名前空間(環境)で作成されたものなので,fun()
を見つけることができません.
同様のことはR6でportable=FALSE
と設定した場合にも起こります.例を示します.
library(R6)
# 環境を作成してパッケージをシミュレートする
pkgA <- new.env()
pkgB <- new.env()
# pkgBではなく,pkgAに関数を作成する
pkgA$fun <- function() 10
ClassA <- R6Class("ClassA",
portable = FALSE,
public = list(
foo = function() fun()
),
parent_env = pkgA
)
# ClassBがClassAを継承する
ClassB <- R6Class("ClassB",
portable = FALSE,
inherit = ClassA,
parent_env = pkgB
)
ClassAのインスタンスを作成すると,期待通り動作します.
a <- ClassA$new()
a$foo()
#> [1] 10
しかし,ClassBはfun()
関数を見つけることができません.
b <- ClassB$new()
b$foo()
#> Error in b$foo() : could not find function "fun"
移植可能なR6
R6はデフォルトではportable=TRUE
とすることで,異なるパッケージ間での継承をサポートしています.以下の例では,再びクラスの親環境を作成して,異なるパッケージをシミュレートしています.
pkgA <- new.env()
pkgB <- new.env()
pkgA$fun <- function() {
"この関数 `fun()` は pkgA にあります"
}
ClassA <- R6Class("ClassA",
portable = TRUE, # デフォルトの設定
public = list(
foo = function() fun()
),
parent_env = pkgA
)
ClassB <- R6Class("ClassB",
portable = TRUE,
inherit = ClassA,
parent_env = pkgB
)
a <- ClassA$new()
a$foo()
#> [1] "この関数 `fun()` は pkgA にあります"
b <- ClassB$new()
b$foo()
#> [1] "この関数 `fun()` は pkgA にあります"
メソッドがスーパークラスから継承されると,そのメソッドはスーパークラスの環境も取得します.言いかえると,メソッドはスーパークラスの環境の中で実行されます.これによってパッケージをまたいだ継承がうまく動作するようになります.
メソッドがサブクラスで定義されている場合には,そのメソッドはサブクラスの環境を取得します.たとえば,ClassCがClassAのサブクラスであり,ClassAのfoo()
メソッドをオーバーライドして独自のfoo()
メソッドを定義するものとします.このメソッドがたまたまClassAのメソッドと同じ見かけをしていて,単にfun()
を呼び出すものであるとしましょう.しかしこの場合はpkgA$fun()
ではなくpkgC$fun()
が見つかることになります.foo()
メソッドと環境をClassAから継承したClassBとは対照的です.
pkgC <- new.env()
pkgC$fun <- function() {
"この `fun()` は pkgC にあります"
}
ClassC <- R6Class("ClassC",
portable = TRUE,
inherit = ClassA,
public = list(
foo = function() fun()
),
parent_env = pkgC
)
cc <- ClassC$new()
# このメソッドはClassCで定義されているのでpkgC$funが見つかる
cc$foo()
#> [1] "この `fun()` は pkgC にあります"
self
の使用
移植不可クラスと移植可能クラスの重要な違いは,移植不可クラスではメンバに対してメンバの名前だけでアクセスできますが,移植可能クラスではメンバへのアクセスに常にself$
かprivate$
が必要だということです.これは継承の実装方法からくる帰結です.
二つのメソッドを持つ移植不可クラスの例を示します.sety()
メソッドは<<-
演算子を使ってプライベートフィールドy
を設定するものであり,getxy()
メソッドはフィールドx
とy
の値を持つベクトルを返すものです.
NP <- R6Class("NP",
portable = FALSE,
public = list(
x = 1,
getxy = function() c(x, y),
sety = function(value) y <<- value
),
private = list(
y = NA
)
)
np <- NP$new()
np$sety(20)
np$getxy()
#> [1] 1 20
移植可能クラスで同じことを試みると誤った結果になります.
P <- R6Class("P",
portable = TRUE,
public = list(
x = 1,
getxy = function() c(x, y),
sety = function(value) y <<- value
),
private = list(
y = NA
)
)
p <- P$new()
# エラーは出ないが,private$yは設定されず,グローバル環境のyが設定されてしまう!
# これは<<-のセマンティクスのせいである.
p$sety(20)
y
#> [1] 20
p$getxy()
#> Error in p$getxy() : object 'y' not found
移植可能クラスでこれを動作させるには,self$x
とprivate$y
を使う必要があります.
P2 <- R6Class("P2",
portable = TRUE,
public = list(
x = 1,
getxy = function() c(self$x, private$y),
sety = function(value) private$y <- value
),
private = list(
y = NA
)
)
p2 <- P2$new()
p2$sety(20)
p2$getxy()
#> [1] 1 20
x
ではなくself$x
を使うと性能では少し損をします,この損失は大多数の場合においては無視できる程度ですが,秒間何万以上ものアクセスがあるような状況では顕在化することがあり得ます.さらなる情報については「性能」のビネットを見てください.
パッケージ間継承の潜在的危険
継承はオブジェクトがMyClass$new()
によってインスタンス化された時に起こります.この時,スーパークラスのメンバが新しいオブジェクトにコピーされます.これは要するに,R6オブジェクトをインスタンス化する時に,そのオブジェクトがスーパークラスの一部分を自身の中に保存するということです.
Rのパッケージビルドの方法のせいで,パッケージのバージョンが変わる時に,R6での継承のふるまいが思いもよらない診断の難しい問題につながってしまうかもしれません.
ClassA
を含むpkgAと,ClassB
を含むpkgBという二つのパッケージがあり,pkgBにはビルド時にClassB
をインスタンス化してobjB
というオブジェクトを作るコードが含まれているとしましょう.これは実行時に関数を呼び出してClassB
をインスタンス化するのとは対照的です.バイナリパッケージがビルドされる際にはパッケージ内のすべてのコードが実行され,結果としてできたオブジェクトがパッケージ内に保存されます.(一般に,オブジェクトにpkgB:::objB
でアクセスできるのであれば,それはビルド時に作成されたということです.)
objB
がビルド時に作成されると,スーパークラスであるpkgA::ClassA
の一部分がobjB
の中に保存されます.このこと自体には問題はありません.しかし,バージョン1.0のpkgAに対してpkgBのビルドとインストールを行った後に,pkgAをバージョン2.0に更新したが,その後pkgBのビルドとインストールは行わなかった,という状況を想像してみてください.すると,pkgB::objB
はバージョン1.0のpkgA::ClassA
のコードを含んでいるが,インストールされているpkgA::ClassA
のバージョンは2.0だということになります.もしobjB
がpkgA
の変更箇所を含むコードを継承していれば問題が起こり得ますが,見つけるのが難しい問題になるかもしれません.
このシナリオはパッケージをCRANからインストールする時に起こる可能性があります.あるパッケージを更新する際に,そのパッケージに依存している下流側のパッケージは更新しないというのは非常によくあることです.私が知る限り,ユーザのコンピュータ上でパッケージが更新された際に,下流側で依存しているパッケージが再ビルドされるように強制する仕組みはRにはありません.
この問題が発生した場合の解決策は,pkgBをバージョン2.0のpkgAに対して再ビルドすることです.CRANでパッケージが更新される際に,下流側で依存しているすべてのパッケージが再ビルドされているのかどうか,私は知りません.もし再ビルドされていないのであれば,CRANにあるpkgAとpkgBのバイナリには互換性がなく,ユーザがpkgBをソースからinstall.packages("pkgB", type = "source")
でインストールしなければならなくなる,ということがあり得ます.
この問題を完全に回避するには,ClassB
のオブジェクトがビルド時にはインスタンス化されないようにしなければいけません.オブジェクトをインスタンス化するのは関数の中だけにするか,パッケージに.onLoad()
関数を追加して,パッケージのロード時にインスタンス化するようにすればよいです.
ClassB <- R6Class("ClassB",
inherit = pkgA::ClassA,
public = list(x = 1)
)
# ロード時に埋めるようにする
objB <- NULL
.onLoad <- function(libname, pkgname) {
# 名前空間がロックされるのはロード後.ここではまだobjBを変更できる.
objB <<- ClassB$new()
}
なぜパッケージのビルド時にClassB
(クラスのインスタンスobjB
ではなく,クラス)がpkgA::ClassA
のコピーを自分の中に保存しないのか,不思議に思われるかもしれません.これはなぜかというと,R6Class()
はinherit
引数については未評価の表現式(pkgA::ClassA
)を保存するようになっており,評価は$new()
が呼び出された時に行うからです.
まとめ
まとめると
- 移植可能クラスでは異なるパッケージをまたいだ継承ができます
- 移植可能クラスではメンバにアクセスするのに常に
self
かprivate
が必要です.self$x
を使うと単にx
とするより遅いため,性能では少し損をするかもしれません.
訳注:翻訳の際,コードは以下の環境で実行した.
devtools::session_info()
#> Session info --------------------------------------------------------------
#> setting value
#> version R version 3.3.2 (2016-10-31)
#> system x86_64, mingw32
#> ui RTerm
#> language (EN)
#> collate Japanese_Japan.932
#> tz Asia/Tokyo
#> date 2017-01-15
#> Packages ------------------------------------------------------------------
#> package * version date source
#> backports 1.0.4 2016-10-24 CRAN (R 3.3.2)
#> devtools 1.12.0 2016-06-24 CRAN (R 3.3.2)
#> digest 0.6.10 2016-08-02 CRAN (R 3.3.2)
#> evaluate 0.10 2016-10-11 CRAN (R 3.3.2)
#> htmltools 0.3.5 2016-03-21 CRAN (R 3.3.1)
#> knitr 1.15.1 2016-11-22 CRAN (R 3.3.2)
#> magrittr 1.5 2014-11-22 CRAN (R 3.3.1)
#> memoise 1.0.0 2016-01-29 CRAN (R 3.3.1)
#> R6 * 2.2.0 2016-10-05 CRAN (R 3.3.2)
#> Rcpp 0.12.8 2016-11-17 CRAN (R 3.3.2)
#> rmarkdown 1.2 2016-11-21 CRAN (R 3.3.2)
#> rprojroot 1.1 2016-10-29 CRAN (R 3.3.2)
#> stringi 1.1.2 2016-10-01 CRAN (R 3.3.2)
#> stringr 1.1.0 2016-08-19 CRAN (R 3.3.2)
#> withr 1.0.2 2016-06-20 CRAN (R 3.3.1)
#> yaml 2.1.14 2016-11-12 CRAN (R 3.3.2)