Mukku John Blog

取り組んでいること を つらつら と

Rプログラミング入門 16回目

前回の作業で、play関数を仕上げましたが、出力が美しくないです。
mukkujohn.hatenablog.com

この出力結果をどうにかしたい部分

  1. シンボルの表示に、print関数を利用しているため、シンボルが返却されない
  2. 賞金=金額なので、$マークを付与したい
> 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と呼ばれます。

  1. ジェネリック関数
  2. ジェネリック関数のメソッド
  3. クラスによる割り当て
クラスによる割り当て

では、どのように、クラスによる割り当てを行っているのでしょうか?
よ~く、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。