Rプログラミング入門 16回目
前回の作業で、play関数を仕上げましたが、出力が美しくないです。
mukkujohn.hatenablog.com
この出力結果をどうにかしたい部分
- シンボルの表示に、print関数を利用しているため、シンボルが返却されない
- 賞金=金額なので、$マークを付与したい
> play() [1] "0" "7" "B" [1] 0
上の問題点をどうにかこうにかして、こうしたいです。
> play() [1] 0 7 B [1] $0
この問題を解消してくれるのが、S3システム
S3システム
Rに組み込まれているクラスシステムのこと。
クラスについては、この記事で触れています。
mukkujohn.hatenablog.com
端的に言うと、これ。
> num <- 1000000000 > print(num) [1] 1e+09 > class(num) <- c("POSIXct","POSIXt") > print(num) [1] "2001-09-09 10:46:40 JST"
属性
クラスは、オブジェクトの属性の1つですが、
オブジェクトが持つ情報を増やすためには、この属性を使い倒します。
play関数の出力結果を保持するone_playオブジェクトを
使って、属性をいじっていきます。
> one_play <- play() [1] "B" "0" "B" > one_play [1] 0 > attributes(one_play) NULL
属性を付与するには、attr関数を利用します。
> attr(one_play, "symbols") <- c("B","0","B") > attributes(one_play) $symbols [1] "B" "0" "B"
特定の属性を指定しない限り、属性を無視してオブジェクトを操作します。
上で付与したsymbols属性を無視して、値だけを操作します。
> one_play + 1 [1] 1 attr(,"symbols") [1] "B" "0" "B"
というわけで、play関数で返すオブジェクトにsymbols属性を付与して返却します。
play <- function(){ symbols <- get_symbols() prize <- score(symbols) attr(prize,"symbols") <- symbols prize }
> two_play <- play() > two_play [1] 0 attr(,"symbols") [1] "0" "BBB" "0"
うん。なんか汚くなってないか。
attr関数を利用して、属性を付与しましたが、
structure関数を使う事でまとめることができます。
> play <- function(){ + symbols <- get_symbols() + structure(score(symbols), "symbols" = symbols ) + }
> three_play <- play() > three_play [1] 0 attr(,"symbols") [1] "0" "0" "0"
属性を付与して、返却することで何が嬉しいのか?
それは、その属性を利用する事で、オブジェクトの扱い方が増えることです。
symbols属性に特化した、出力を整形する関数を作成します。
その関数がこちら。
slot_display <- function(prize){ # シンボルの抽出 symbols <- attr(prize,"symbols") # シンボル属性の文字列ベクトルを、1つの文字に連結する symbols <- paste(symbols, collapse = " ") # さらに、賞金を付与する。セパレータが改行文字と$マーク string <- paste(symbols, prize, sep = "\n$") # 出力をコマンドラインに表示。ただし、""は使わない。 cat(string) }
まぁ、こんな感じですね。
> slot_display(two_play) 0 BBB 0 $0 > slot_display(three_play) 0 0 0 $0 > slot_display(play()) B BB 0 $0
ただし、この方法にも問題点があります。
属性を利用した、出力を整形する関数を作り、
その関数でインターセプトしないといけない部分です。
ジェネリック関数
説明に入る前に、こちらをご覧ください。
> print(pi) [1] 3.141593 > pi [1] 3.141593
print関数で、piオブジェクトを表示しても、
直接、piオブジェクトを表示しても、出力結果が同じです。
この出力結果が同じになるところに、ジェネリック関数(S3システム)が隠れています。
しつこいですが、class属性により、出力内容が変わっています。
> num <- 1000000000 > print(num) [1] 1e+09 > class(num) <- c("POSIXct","POSIXt") > print(num) [1] "2001-09-09 10:46:40 JST"
なので、symbolsクラス属性がある場合は、
print関数の動作を、symbolsクラス属性に特化した形にすればよいのです。
これができるのが、ジェネリック関数。
メソッド
print関数に注目してみます。
> print function (x, ...) UseMethod("print") <bytecode: 0x0000000012702f50> <environment: namespace:base>
どうやら、print関数は、UseMethodという関数を呼んでいます。
print関数が持つ、メソッドに注視してみます。
> methods(print) [1] print.acf* [2] print.anova* [3] print.aov* [229] print.xngettext* [230] print.xtabs* see '?methods' for accessing help and source code
ありすぎ・・・
さらに、列挙されているこんなメソッドに注視してみます。
> print.POSIXct function (x, ...) { max.print <- getOption("max.print", 9999L) if (max.print < length(x)) { print(format(x[seq_len(max.print)], usetz = TRUE), ...) cat(" [ reached getOption(\"max.print\") -- omitted", length(x) - max.print, "entries ]\n") } else print(format(x, usetz = TRUE), ...) invisible(x) } <bytecode: 0x00000000126926e0> <environment: namespace:base>
つまり、print関数は、指定された引数のオブジェクトのクラスを調べ
そのクラスに該当する、メソッドに引数を渡して実行しています。
この仕組みが、S3システム*1と呼ばれます。
クラスによる割り当て
では、どのように、クラスによる割り当てを行っているのでしょうか?
よ~く、print関数が持っているメソッドを眺めるとわかります。
すごい単純。
メソッドを使う関数名.クラス名です。
例:
- print.factor
- print.POSIXct
- print.POSIXlt
というわけで、まずは、出力するオブジェクトにクラス属性を付与します。
ここでは、one_playオブジェクトに、slotsクラス属性を付与しています。
> one_play <- play() > class(one_play) <- "slots" > one_play [1] 0 attr(,"symbols") [1] "0" "0" "0" attr(,"class") [1] "slots"
次に、print関数にslotsクラス用のメソッドを追加します。
追加する際には、print関数本体の引数に合わせて作成する必要があります。
念のため、引数確認。
> args(print) function (x, ...) NULL
いざ、print関数にslotsクラス用のメソッド追加。
print.slots <- function (x, ...){ slot_display(x) }
(手抜き。上で作ったslot_display関数呼び出すダケー)
> print(one_play) 0 0 0 $0 > one_play 0 0 0 $0
ほら。出力もばっちり。
というわけで、最初の問題点に対する答えがこちら。
play <- function(){ symbols <- get_symbols() structure(score(symbols), symbols = symbols, class = "slots") }
> play() B B B $10 > print(play()) 0 0 0 $0
*1:Sのバージョン3で作成された。S → S-PLUS → R。