StatsFragments

Python, R, Rust, 統計, 機械学習とか

R {R6} で 別クラス同士の演算子を定義したい

先日の Tokyo.R で紹介されていた {R6} パッケージが使いやすそうだったため、自分の パッケージ でも 使ってみるべく試した。

R6パッケージの紹介―機能と実装

作りたいのは 複数ggplot インスタンスをまとめて描画するコンテナクラス。{ggplot2} では 種類の異なる 複数のプロットをサブプロットにできないため、自分のパッケージでは以下のようなコンテナを作っている。普通にサブプロット描画したいだけならこんなクラスは必要ないが、{ggplot2} と同じように 加算演算子 + を使って テーマの変更とかしたい。これは以下のような感じで動く。

p1 <- qplot(Petal.Width, Petal.Length, colour = Species, data = iris)
p2 <- qplot(Sepal.Width, Petal.Length, colour = Species, data = iris)
mp <- new('ggmultiplot', plots = list(p1, p2))
mp + theme_bw()

f:id:sinhrks:20150228215640p:plain

S4 class で書く

現在は 以下のような S4 クラスでの定義を使っている。パッケージ中では行数/列数指定時のレイアウト調整などもやっているが、今回は関係ないので省略。

library(ggplot2)
library(gridExtra)

# S4 クラス定義
setClass('ggmultiplot', representation(plots = 'list'))

# ggmultiplot に対する加算演算子を定義
setMethod('+', c('ggmultiplot', 'ANY'),
  function(e1, e2) {
    # 第二引数を plots の各プロットに順番に適用
    plots <- lapply(e1@plots, function(x) { x + e2 })
    new('ggmultiplot', plots = plots)
})

# print メソッドを定義
# gridExtra::grid.arrange を使って複数のプロットを描画
setMethod('print', 'ggmultiplot',
  function(x) {
    nplots = length(x@plots)
    if (nplots==1) {
      print(x@plots[[1]])
    } else {
      args <- c(x@plots, list(ncol = 2))
      do.call(gridExtra::grid.arrange, args)
    }
})

# show メソッドを定義
setMethod('show', 'ggmultiplot', function(object) { print(object) })

{R6} class で書く

これを {R6} クラスに書き換えるとこんな感じ。クラス定義自体はシンプルになってうれしい。S4 用の setMethod{R6} に対して使えないため、加算演算子は S3 の総称関数として定義する必要がある。

library(R6)

# クラス定義
ggmultiplotR6 <- R6::R6Class('ggmultiplotR6',
  public = list(

    plots = list(),
    
    initialize =  function(plots) {
      self$plots <- plots
    },

    print = function() {
      nplots = length(self$plots)
      if (nplots==1) {
        print(self$plots[[1]])
      } else {
        args <- c(self$plots, list(ncol = 2))
        do.call(gridExtra::grid.arrange, args)
      }
    }
  )
)

`+.ggmultiplotR6` <- function(e1, e2) {
  if ('ggmultiplotR6' %in% class(e1)) {
    plots <- lapply(e1$plots, function(x) { x + e2 })
    return(ggmultiplotR6$new(plots = plots))
  } else {
    e2name <- deparse(substitute(e2))
    if (ggplot2::is.theme(e1)) return(ggplot2:::add_theme(e1, e2, e2name))
    else if (ggplot2::is.ggplot(e1)) return(ggplot2:::add_ggplot(e1, e2, e2name))
  }
}

が、これをそのまま実行すると 以下の通りエラーになる。

p1 <- qplot(Petal.Width, Petal.Length, colour = Species, data = iris)
p2 <- qplot(Sepal.Width, Petal.Length, colour = Species, data = iris)
mp <- ggmultiplotR6$new(plots = list(p1, p2))
mp + theme_bw()
#  以下にエラー mp + theme_bw() :  二項演算子の引数が数値ではありません 
#  追加情報:  警告メッセージ: 
#  メソッド ("+.ggmultiplotR6", "+.gg") は "+" に対しては矛盾しています  

エラーの理由は、 S3 総称関数が 複数の引数を受け取った場合、すべて引数について対応する関数を探すため。上の例では、ggmultiplotR6gg インスタンス同士の加算になり、定義した +.ggmultiplotR6{ggplot2} 内で定義されている +.gg が衝突してエラーになる。詳細は {R6} 作者の方の以下の説明がわかりやすい。

リンク先に書いてある通り、演算子に対応する関数の定義を同一にすれば回避できる。

`+.gg` <- `+.ggmultiplotR6`

mp <- ggmultiplotR6$new(plots = list(p1, p2))
mp + theme_bw()
# OK (出力省略)

補足 演算子がパッケージ中で定義される場合は、上の方法ではなく.onload 中で registerS3method する必要がある (詳細はリンク先)。

まとめ

{R6} を使う場合は クラス同士の演算は 単一の S3 総称関数として定義する必要がある。そのため、クラスが増えてくると定義が結構複雑になりそう。

また、上の例のように 異なるパッケージ中のクラスとの演算を定義したい場合、相手側の 演算子定義と衝突しないような配慮が必要。相手側を上書きするのはあまりうれしくないので、自分のパッケージは当面 S4 を使うことにした。

ということで {R6}演算子を使いたい場合は少し気をつけたほうがよさげ。

2/28 追記

下のやり方でいけるかも。後で試します。

2/29 追記

試してみた。setOldClass を以下のように使うことで、新規で定義した関数は期待通り setMethod できる。

class(mp)
# [1] "ggmultiplotR6" "R6"   

setOldClass(c('ggmultiplotR6', 'R6'))
isClass('ggmultiplotR6')
# [1] TRUE        

setGeneric("foo", signature = "x",
  def = function(x) standardGeneric("foo")
)
# [1] "foo"

setMethod("foo", c(x = "ggmultiplotR6"),
  definition = function(x) {
    "I'm the method for `R6`"
})
# [1] "foo"

foo(mp)
# [1] "I'm the method for `R6`"

しかしながら、既存の演算子への setMethod には効果がない。出力を見た感じ、S3 総称関数の定義が使われているようだ。

setMethod('+', c(e1 = 'ggmultiplotR6', e2 = 'ANY'),
  function(e1, e2) {
    plots <- lapply(e1$plots, function(x) { x + e2 })
    ggmultiplotR6$new(plots = plots)
})
# [1] "+"

mp + theme_bw()
# NULL

何かいいやり方がないかは探したい。