data.frameを拡張するには 成功編(S4でふつうに)

前回までのあらすじ

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

  • data.frameと同じようにふるまうけど、スキーマ情報やリビジョン番号などのメタデータを持ってる、みたいなクラスをつくりたい
  • S3でふわっとやろうとしたけど無理だった
  • ということでS4でやってみよう

(この実装を見てやり方を知りました:EML/R/data.set.R at master · ropensci/EML

S4でやってみる

S4のクラス

S4では、スーパークラスを指定できます。
スーパークラスとしてdata.frameを指定すれば、
data.frameのような挙動をするクラスをつくることができます。

S4のクラス宣言はsetClassを使います。

> args(setClass)
function (Class, representation = list(), prototype = NULL, contains = character(), 
    validity = NULL, access = list(), where = topenv(parent.frame()), 
    version = .newExternalptr(), sealed = FALSE, package = getPackageName(where), 
    S3methods = FALSE, slots)

containsは、スーパークラス名を指定するパラメータです。
これにdata.frameを指定します。

slotsは、クラスのプロパティ名とクラスを宣言します。
ここに、必要なメタデータを入れます。

こんな感じです。(参照元のコードはこのへん

> setClass("myclass", slots = c(url="character"), contains = "data.frame")

S4のインスタンス作成

S4のインスタンスは、newでつくります。

> a <- new("myclass")

なんですが、data.frameと同じようにmyclass(...)という感じでインスタンスをつくりたいので、
S3っぽいコンストラクタを定義します。(参照元のコードはこのへん

> myclass <- function(url, ...) { new("myclass", data.frame(...), url=url) }

これを使ってmyclassインスタンスをつくると、data.frameと同じ挙動をします。

> a <- myclass(x=rnorm(10), "http://notchained.hatenablog.com")
> a
Object of class "myclass"
             x
1  -1.30937798
2  -0.45263021
3  -1.70143079
4  -0.22452977
5   0.71383416
6  -0.07953994
7   0.50595564
8  -2.90918912
9  -2.10720904
10 -0.09992661
Slot "url":
[1] "http://notchained.hatenablog.com"
> a[1,]
[1] -1.309378
> a[1:5,]
[1] -1.3093780 -0.4526302 -1.7014308 -0.2245298  0.7138342
> subset(a, x > 0)
          x
5 0.7138342
7 0.5059556

S4のメソッド

めんどくさい(し、あんまりちゃんと理解できていない)ので説明は端折りますが、 S4クラスのメソッド定義は、

  1. standardGenericジェネリック(総称型関数)をつくる
  2. setGenericジェネリックを宣言する
  3. setMethodでメソッドを定義する

という順序を踏むらしいです。

たとえば、fetchというメソッドを定義したければ、
まずstandardGenericfetchジェネリックを作成し、
それをsetGenericで宣言します。 以下の2つはどちらも同じことです。

> setGeneric("fetch", function(x) standardGeneric("fetch"))
[1] "fetch"
> fetch <- function(x) standardGeneric("fetch")
> setGeneric("fetch")

そして、setMethodで、myclassfetchを実装します。

> setMethod("fetch", "myclass", function(x) getURL(x@url))

これでfetchが使えるようになりました。

まとめ

まあ要はdata.frameを継承したクラスをつくりたくて、
それをするにはS3ではむりでS4でやりましょう、ということでした。

おしまい。