0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[翻訳]R6 vignette: 移植可能R6クラスと移植不可R6クラス

Last updated at Posted at 2017-01-15

この文書は,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()メソッドはフィールドxyの値を持つベクトルを返すものです.

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$xprivate$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だということになります.もしobjBpkgAの変更箇所を含むコードを継承していれば問題が起こり得ますが,見つけるのが難しい問題になるかもしれません.

このシナリオはパッケージを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()が呼び出された時に行うからです.

まとめ

まとめると

  • 移植可能クラスでは異なるパッケージをまたいだ継承ができます
  • 移植可能クラスではメンバにアクセスするのに常にselfprivateが必要です.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)
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?