S4クラスとSICP

Rにはオブジェクト指向プログラムのためのフレームワークとしてS4というものがあるらしい。と、いうことは長らく知っていたけど、説明を読んでもさっぱり分からなかったので、全く使ったことが無かった。ところが、SICPを読んでみたら何となく分かったので、メモ。
SICP2.4節2.5節辺りが該当する箇所。

data-directed programming

S4フレームワークは、要するにSICPの中でdata-directed programmingと呼ばれているスタイルを実装したものである(と思う)。これは、ポリモーフィズムの実現のさせ方の一つの方法である。しかし、普段なじみのあるC++/JAVA式の実現のさせ方とは雰囲気が違う。S4が理解出来なかったのは、オブジェクト指向/ポリモーフィズムといった時にC++Javaのようなものしか想像出来なかったところに原因があるような気がする。なので、C++式とdata-directed programmingスタイルの違いをここにまとめておく。

C++式のポリモーフィズムの実現

C++式のオブジェクト指向では、次のような仕組みでポリモーフィズムが実現される。

  • データとそれを操作する関数をひとまとめにしたものとしてオブジェクトがある。
  • オブジェクトを利用する側は、オブジェクトが持っている関数の名前とシグネチャ(i.e. 戻り値の型と引数の型)を知っている。
  • 但し、中身でどのような操作が行われるかには、全く感知しない。

従って、あるオブジェクトを利用しているプログラムがあった時、そのオブジェクトを、同じ名前とシグネチャを持つ関数を含んだ別のオブジェクトに自由に置き換えることが出来る(少なくとも形式上は)。

data-directed programmingスタイルのポリモーフィズムの実現

data-directed programmingスタイルでは、次のようになる。

  • オブジェクトには「データ」と「タグ」が含まれる。
  • いくつかの関数名は、generic functionとして処理系に登録されている。
  • 処理系の中では、(generic functionの関数名, オブジェクトのタグ)のペアと関数の実体を紐付けたテーブルがある。
  • generic functionがオブジェクトに適用されると、その関数名とオブジェクトのタグに紐付けられた関数がオブジェクトのデータに適用される。

従って、generic functionをオブジェクトに適用しているプログラムがあった時、そのオブジェクトを別のタグを持つオブジェクトに置き換えると、別の関数が発動されることになる。

Rでの例

## A, B, Cというクラスを宣言(クラス名がタグの役割を果たす)
setClass("A", representation(a="numeric"))
setClass("B", representation(b="numeric"))
setClass("C", representation(c="numeric"))

## "func1" は generic function だと処理系に教える
setGeneric("func1", function(x)standardGeneric("func1"))

func1_for_A <- function(x){
  cat("func1 for A\n")
}
func1_for_B <- function(x){
  cat("func1 for B\n")
}

## 処理系のテーブルの中に関数の実体を登録
setMethod("func1", signature(x="A"), func1_for_A)
setMethod("func1", signature(x="B"), func1_for_B)

obj.a <- new("A", a=1)
obj.b <- new("B", b=1)
obj.c <- new("C", c=1)

func1(obj.a) # ;=> func1 for A
func1(obj.b) # ;=> func1 for B
## func1(obj.c) # (func1, C)の組み合わせは未登録なのでエラー

## 複数引数も登録出来る
setGeneric("func1", function(x, y)standardGeneric("func1"))

func1_for_AB <- function(x, y){
  cat("func1 for AB\n")
}
func1_for_BC <- function(x, y){
  cat("func1 for BC\n")
}
setMethod("func1", signature(x="A", y="B"), func1_for_AB)
setMethod("func1", signature(x="B", y="C"), func1_for_BC)

func1(obj.a, obj.b) # ;=> func1 for AB
func1(obj.b, obj.c) # ;=> func1 for BC
## func1(obj.c, obj.a) # 未登録エラー

standardGenericがSICPapply-genericに対応している。