data.frameを拡張するには 失敗編(S3でゆるふわ)

リモートにあるデータベースとシンクするdata.frameをつくりたくて調べて分かったことのメモ。
(まだぜんぜん完成してないのでぼんやり書きます)

シンクするためには例えば、以下のような情報を保持する必要があります。

  • スキーマ情報
  • シンク先のURLまたはIP
  • 最終更新した日付
  • リビジョン番号

でも、挙動はdata.frameと同じであってほしいです。
サブセットを作るのはsubsetとか使いたいし、ggplotでプロットもしたいです。

そのためにいちいち自前でsubsetとかggplotを実装しなければいけないとか言われると気が遠くなります。
ということで、外見上はdata.frameとして見えるようにする方法を調べました。

結論から言うと、同じことを考える人はいるもので、わりとあっさり方法は見つかりました。 というかぶっちゃけ今回書くことは↓のコピペみたいなもんです。

Extending Data Frame Class
EML/R/data.set.R at master · ropensci/EML

そもそもRのオブジェクトって?

ぜんぜん理解できていないので適当なこと書きますが、、
HadleyさんのAdvanced Rによると、Rには4種類のオブジェクトシステムがあります。

  • 組み込み型
  • S3 → 昔からあるやつ。クラスの定義はゆるふわ。
  • S4 → 新しいやつ。クラスの定義は厳密でめんどくさい。
  • Reference classオブジェクト指向言語っぽい感じ。

できればゆるふわですませたいものです。

data.frameってS3? S4?

Extending Data Frame Classでは、
data.frameがS3かS4か調べています。

data.frameコンストラクタはS3のdata.frame()と、S4のnew("data.frame")があります。
data.frame()でつくるとisS4FALSEになって、一見S3っぽいのですが、
slotを持っていることからS4ではないか、という推測がされています。

> a <- data.frame(x=rnorm(10))
> isS4(a)
[1] FALSE
> slotNames(a)
[1] ".Data"     "names"     "row.names" ".S3Class"

まあでもS3っぽく動いたりもするので、S3的なやり方とS4的なやり方、両方試してみます。

S3でやってみる

S3のクラス

S3のクラス定義、というのは特にありません。
structure()インスタンス生成時にclassを設定するか、
class<-()であとからクラスを設定するか、
いずれにせよクラス名はただの文字列です。

> a <- structure(list(), class="myclass1")

> class(a) <- "myclass2"

これを毎回手でやると、めんどくさかったり、
クラス名を打ち間違えても特にエラーは出なかったりするので
実用上はコンストラクタを定義します。

> myclass <- function(...) {structure(list(...), class="myclass")}
> a <- myclass(x=1)
> class(a)
[1] "myclass"

S3のメソッド

S3のメソッドは、method.myclass <- function(x) {...}みたいな感じで宣言します。
例えば、myclassインスタンスAについて、中身を表示するためにprint(A)を実行するとします。
print.myclassが定義されていればそれが呼ばれ、なければprint.defaultが使われます。
逆にいえば、myclassprint結果を標準から変えたければprint.myclassを定義します。

ちょっと脱線すると、baseパッケージの関数を上書きすることも可能です。
↓こんな感じ

> a <- data.frame(x=rnorm(10))
> class(a)
[1] "data.frame"
> a[1:3,]
[1] -0.084118386 -0.003389443  0.535042087
> a[1,11]
NULL

> `[.data.frame` <- function(x, ...) 'Overwritten.'
> a[1:3, ]
[1] "Overwritten."
> a[1,11]
[1] "Overwritten."

> rm(`[.data.frame`)

S3的にdata.frameを拡張する

結論から言うと、

  • data.frameを上書きすれば拡張はできる
  • mydata.frameみたいなのをdata.frameと同じように挙動させるのは難しい

という感じで、やりたいことは満たせませんでした。

メタデータ的なものを追加するのはattr()を使えばできます。
ここで設定した値はdata.frameの影響を受けません。

> a <- data.frame(x=2)
> a[1, ]
[1] 2
> attr(a, "url") <- "https://notchained.hatenablog.com/"
> attr(a, "url")
[1] "https://notchained.hatenablog.com/"
> a[1, ]
[1] 2
> a$url
NULL

問題は、クラスをmyclassとかにしてしまうと、 当然ながらもうdata.frameではいられないことです。

> class(a) <- "myclass"
> a[1, ]
Error in a[1, ] : incorrect number of dimensions

まあ、data.frameのメソッドを追加していけば、 やりたいことができるといえばできます。
たとえば、データをリモートからとってくるfetch.myclassみたいなの関数を定義しようとしていたとすると、

fetch.data.frame <- function(x) {getURL(attr(x, "url"))}

みたいな感じのメソッドを定義してやれば、data.framefetchすることはできます。

ただ、ここでやりたいことは「外見的にdata.frameに見えるクラスをつくりたい」ということなので
data.frameを俺拡張する」というのは少し違いますよね。
副作用が怖そうだし。。

まとめ

ということで、S3でもいいところまでいきましたが、やっぱり限界がありました。
やっぱりそうですよね(ため息)。

長くなってしまったので、成功編(S4でふつうに)はまた次回!

追記:次回は http://notchained.hatenablog.com/entry/2014/03/02/013147 です。