 | レベル: 中級 David Mertz, Ph.D (mertz@gnosis.cx), Author, Gnosis Software, Inc.
2006年 1月 26日 Rは、フリー・ソフトウェアとしてリリースされている機能豊富な統計環境であり、プログラミング言語、対話型シェル、充実したグラフ機能などが含まれています。この記事では、Davidによる以前の2回の記事 (Brad Hunttingとの共著)に続いて、Rのオブジェクト指向とRのその他の一般的なプログラミング概念を考察します。
このシリーズの最初の2回の記事では、かなり「現実世界」でのRの用途を見てきました。これらの記事の共著者が収集した大量の温度データを使って、さまざまな統計分析機能とグラフ機能を調べました。これらの記事で述べたように、われわれが実際に調べたのは、Rの深くて機能豊富な統計ライブラリーのほんの表面に過ぎません。
今回の記事では、統計分析そのものの詳細な考察はひとまず置いておきたいと思います(その主な理由は、私には統計の基礎知識がないため、最も重要な技法を決めることができないからです。私の以前の共著者であるBrad Hunttingや読者の皆さんの方が、この分野の知識を豊富に持っています)。最初の2回の記事で紹介した機能豊富な統計概念を補足するものとして、Rの基本的言語機能について、いくつかの指針を読者に提供したいと思います。これまでの記事では、Rの関数型プログラミングについて述べてきましたので、多くの読者の方が命令型のオブジェクト指向言語に詳しくなるのではと思っています。
また、これまでは、かなり場当たり的にRを見てきました。今回の記事では、R開発のための再利用可能なモジュール方式のコンポーネントの作成について述べます。
基本に戻る
Rのオブジェクト指向の概念に入る前に、Rのデータと関数の復習をしましょう。Rのデータについて覚えておかなければならない肝心なことは、「すべてがベクトルである」ということです。行列、配列、data.framesなど、一見、ベクトルとは違うように見えるオブジェクトも、実際には、特殊な扱い方をRに対して指示する余分な(可変性の)属性を持つベクトルに過ぎません。
次元(dim)は、(いくつかの)Rベクトルがもつ最も重要な属性の1つです。関数matrix()、array()、およびdim()は、簡単に言うと、ベクトルの次元を設定するための便利な関数です。同様に、RのOOPシステムを煎じ詰めると、オブジェクトのclass属性になります。
では、次元設定の復習として、まず、リスト1のコードを見てみましょう。
リスト1.ベクトルの作成と次元の割り当て
> v = 1:1000
> typeof(v)
[1] "integer"
> attributes(v)
NULL
> dim(v) = c(10,10,10) # (Re)dimension
> attributes(v)
$dim
[1] 10 10 10
> v2 = matrix(1:1000, nrow=100, ncol=10)
> typeof(v2)
[1] "integer"
> attributes(v2)
$dim
[1] 100 10
> attr(v2,'dim') = c(10,10,10) # Redimension
> attributes(v2)
$dim
[1] 10 10 10
|
要するに、ベクトルにdim属性をアタッチするには、いくつかの便利な構文がありますが、実際には、シンタックス・シュガーが行うのはそれだけです。
「すべてがベクトル」というRのアプローチで混乱を招きやすいのは、行単位と列単位の操作には直感とのずれがあることです。たとえば、2D配列(行列)を作成するのは簡単ですし、単一の列または行を操作するのも簡単です。
リスト2.行列ベクトルと行単位の操作
> m = matrix(1:12, nrow=3, ncol=4)
> m
[,1] [,2] [,3] [,4]
[1,] 1 4 7 10
[2,] 2 5 8 11
[3,] 3 6 9 12
> sum(m) # sum of all elements of m
[1] 78
> sum(m[1,]) # sum of first row
[1] 22
|
しかし、各行を合計するベクトルを作成したい場合、次のようにしたくなるかもしれません。
リスト3.複数の行単位操作を実行するための誤った方法
> sum(m[c(1,2,3),]) # NOT sum of each row
[1] 78
|
ここでループを作成することもできますが、Rの関数型のベクトル指向操作にはなじみません。代わりに、関数apply()を使用するのがコツです。
リスト4.行単位の操作のためのapply() 関数
> apply(m, 1, sum) # by row
[1] 22 26 30
> apply(m, 2, sum) # by column
[1] 6 15 24 33
> apply(m, c(1,2), sum) # by column AND row (sum of each single cell)
[,1] [,2] [,3] [,4]
[1,] 1 4 7 10
[2,] 2 5 8 11
[3,] 3 6 9 12
# Kinda worthless to sum each single cell, but what about this:
> a = array(1:24,c(3,4,2))
> apply(a, c(1,2), sum) # sum by depth in 3-D array
[,1] [,2] [,3] [,4]
[1,] 14 20 26 32
[2,] 16 22 28 34
[3,] 18 24 30 36
> apply(a, c(3), sum) # sum of each depth slice
[1] 78 222
|
無限シーケンス
まったく実践上の理由からですが、あると便利な構造体は、数の無限シーケンスです。たとえば、前回までの記事の共著者はモンテカルロ積分法の分析を行っていましたが、彼の目的では、無限に長い乱数シーケンスが役立ちました。必要な無限シーケンスのタイプは、必要に応じて新しい数を生成できるというだけではありません。以前に参照した特定の要素を再び参照して、以前と同じ値を参照できることも必要です。
もちろん、どんなコンピューター言語もコンピューターも、無限シーケンスを格納できるわけではありません。格納できるのは、レイジーで制約のないシーケンスです。具体化されたリストに要素を追加できるのは、アクセスが必要なときだけです。たとえば、Pythonでは、必要に応じて内部リストを拡張するカスタムの.__getitem__()メソッドでリスト状オブジェクトを作成することによって、これを達成できます。Haskellでは、レイジーさは言語そのものに深く組み込まれています。実際、すべてがレイジーです。私のHaskellチュートリアル(「参考文献」)では、すべて素数のリストを作成する例を使用しました。
リスト5.「エラトステネスのふるい」によるHaskell素数リスト
primes :: [Int]
primes = sieve [2 .. ]
sieve (x:xs) = x : sieve [y | y <- xs, (y `rem` x)/=0]
|
無限という点では、RはHaskellよりもPythonに近いです。必要に応じて、より多くのリスト要素を明示的に作成する必要があります。OOPセクションがベクトル・インデックスそのものに舞台裏のメカニズムを開始させるのを待つ必要があります。それでも、必要な段階はそれほど多いわけではありません。
リスト6.ベクトルとそれを動的に拡張する手段の定義
inf_vector = rnorm(10, 0, 1) # arbritrarily start w/ init 10 items
assure <- function(index) {
extend_by = max(index-length(inf_vector), 0)
extras = rnorm(extend_by, 0, 1)
v <- c(inf_vector, extras)
assign("inf_vector", v, env=.GlobalEnv)
return(index)
}
getRand <- function(index) {
assure(index)
return(inf_vector[index])
}
|
おそらく、望ましい使い方は、getRang()ラッパー関数を通じて値にアクセスすることです。スライスを使うか、計算値を使うか、または単一のインデックスを使うか、自由に決めることができます。
リスト7.無限仮想ベクトルへのプロキシとしてのラッパー関数の使用
> getRand(3) # Single index
[1] 0.5557101
> getRand(1:5) # Range
[1] -0.05472011 -0.30419695 0.55571013 0.91667175 -0.40644081
> getRand(sqrt(c(4,16))) # Computed index collection
[1] -0.3041970 0.9166717
> getRand(100) # Force background vector extension
[1] 0.6577079
|
好みによって、要素を使用する前に、ベクトルが十分に大きいことをassure()するだけでもかまいません。
リスト8.アクセス前のベクトルの拡張(必要な場合)
> assure(2000)
[1] 2000
> inf_vector[1500]
[1] 1.267652
|
オブジェクト指向R
Rは、完全に汎用のオブジェクト指向プログラミングですが、これを理解するには、少し戻って、OOPとは何かを考える必要があります。Java™やC++、あるいはPython、Ruby、Smalltalkなどの言語を使用している人は、オブジェクト指向に対して、どちらかというと固定されたイメージを持っているかもしれません。間違ってはいませんが、1つのモデルに限定して考えているようです。
OOPに対するRのアプローチは、クラス階層ではなく汎用関数に基づいています。この概念は、LispのCLOSを使ったことがある読者や、Pythonを使用した複数のディスパッチについての私の記事(「参考文献」)を読んだことがある人には、なじみやすい概念でしょう。あいにく、Rのアプローチはまだ単一ディスパッチ・アプローチです。その点では、前述した「伝統的」言語(C++、Javaなど)と同じです。
この記事では深入りしませんが、Rの最近のバージョンには、いわゆる「フォーマル・メソッド」を定義して操作するmethodsという名前のパッケージが付属していることを述べておかなければなりません。多くの点で、これらのフォーマル・メソッドを使用するには、伝統的なOOP言語で見つけた規則(と制限)のほとんどを踏襲しなければなりません。いずれにしても、RのフォーマルOOPは、この記事でお話しする「インフォーマルOOP」を土台としています。methodsパッケージは、まだ暫定的なものと言えますが、適度にひねりを加えられたバージョンが、Rの今後のバージョンに含められるのは確かなようです。さらに詳しい背景情報については、「参考文献」を参照してください。
OOPそのものを理解するうえで忘れてはならないことは、OOPは実際には、具体的な継承の問題ではなく、より一般的なディスパッチの決定の問題だということです。すなわち、伝統的なOOP言語でobj.method()を呼び出すと、オブジェクトのメソッド解決命令(MRO)を検索して、メソッド.method()を持つobjの「最初の」祖先クラスを見つけます。
「最初の」が何を意味するかは、見た目より重大な問題です(PythonでのMRO設計の進化については、「参考文献」を参照してください)。Rも同じ決定を下しますが、継承の概念をひっくり返しています。すなわち、Rは、本体でさまざまなメソッドを定義してオーバーライドする一群のクラスではなく、操作対象のオブジェクトのタイプを記述するタグを持った汎用関数のファミリーを作成します。
汎用関数
単純な例として、whoami()という汎用関数と、それをディスパッチするタグ付きメソッドを作成しましょう。
リスト9.汎用関数とタグ付きメソッドの作成
#------------- Create a generic method
> whoami <- function(x, ...) UseMethod("whoami")
> whoami.foo <- function(x) print("I am a foo")
> whoami.bar <- function(x) print("I am a bar")
> whoami.default <- function(x) print("I don't know who I am")
|
ここでのポイントは、Rのオブジェクトはすべて、0、1、または2つ以上のクラスに属することができるということです。具体的に言うと、(特定のメソッドに対して相対的な)特定のオブジェクトのMROは、class属性の名前付きクラス(ある場合)のベクトルに過ぎません。たとえば、次のとおりです。
リスト10.クラス・メンバーシップによるオブジェクトのタグ付け
> a = 1:10
> b = 2:20
> whoami(a) # No class assigned
[1] "I don't know who I am"
> attr(a,'class') <- 'foo'
> attr(b,'class') <- c('baz','bam','bar')
> whoami(a)
[1] "I am a foo"
> whoami(b) # Search MRO for defined method
[1] "I am a bar"
> attr(a,'class') <- 'bar' # Change the class of 'a'
> whoami(a)
[1] "I am a bar"
|
伝統的な継承言語と同様、オブジェクトは、それが呼び出すすべてのメソッドについて同じクラスを利用する必要はありません。従来、ChildがMomとDadを継承する場合、Childタイプのオブジェクトは、Momの.meth1()とDadの.meth2()を利用することができます。当然、Rでもこれが可能ですが、MomとDadは実体のあるものではなく、名前に過ぎません。
リスト11.メソッドごとのディスパッチ解決
> meth1 <- function(x) UseMethod("meth1")
> meth1.Mom <- function(x) print("Mom's meth1")
> meth1.Dad <- function(x) print("Dad's meth1")
> meth2 <- function(x) UseMethod("meth2")
> meth2.Dad <- function(x) print("Dad's meth2")
> attr(a,'class') <- c('Mom','Dad')
> meth1(a) # Even though meth1.Dad exists, Mom comes first for a
[1] "Mom's meth1"
> meth2(a)
[1] "Dad's meth2"
|
祖先の包含
継承構文による暗黙的な解決に頼るのではなく、オブジェクトのMROを明示的に指定しなければならないと言うのは、窮屈に思えるかもしれません。実際には、最小限のラッパー関数によって、継承ベースのMROを実に簡単に実装することができます。私がリスト11で使用したMROは、おそらくベストな例ではありませんが(「参考文献」のSimionatoのエッセーを参照)、考え方は理解できると思います。
リスト12.最小限のラッパー関数による継承ベースのMROの実装
char0 = character(0)
makeMRO <- function(classes=char0, parents=char0) {
# Create a method resolution order from an optional
# explicit list and an optional list of parents
mro <- c(classes)
for (name in parents) {
mro <- c(mro, name)
ancestors <- attr(get(name),'class')
mro <- c(mro, ancestors[ancestors != name])
}
return(mro)
}
NewInstance <- function(value=0, classes=char0, parents=char0) {
# Create a new object based on initial value,
# explicit classes and parents (all optional)
obj <- value
attr(obj,'class') <- makeMRO(classes, parents)
return(obj)
}
MaternalGrandma <- NewInstance()
PaternalGrandma <- NewInstance()
Mom <- NewInstance(classes='Mom', parents='MaternalGrandma')
Dad <- NewInstance(0, classes=c('Dad','Uncle'), 'PaternalGrandma')
Me <- NewInstance(value='Hello World', 'Me', c('Mom','Dad'))
|
リスト12のコードを実行すると、次のようになります。
リスト13.継承によって生成されたMROを持つオブジェクト
> print(Me)
[1] "Hello World"
attr(,"class")
[1] "Me" "Mom" "MaternalGrandma" "Dad"
[5] "Uncle" "PaternalGrandma"
|
従来のクラス/インスタンス関係のアプローチに従いたい場合は、作成するクラスの名前(classes引数のMomなど)を含めます。実際、各クラスそのものが完全に良好なオブジェクトであるとすると、上記のシステムは、クラス・ベースのシステムよりもプロトタイプ・ベースのOOPシステムに近いものです。
やはり、システム全体に柔軟性があるため、あらゆるバリエーションを包含できます。必要であれば、クラス・オブジェクトをインスタンス・オブジェクトから切り離すこともできます。もう1つの属性(typeはclassまたはinstanceである、など。ユーティリティー関数でチェックします)をアタッチしたり、その他の方法によって、命名規則があるクラス(Momとmomなど)を区別することもできます。
無限ベクトルの復習
OOPの足場ができたので、以前に述べた無限ベクトルが理解しやすくなったはずです。最初のソリューションは完全に有効ですが、さらにシームレスで不可視な無限ベクトルがあれば、なおよいかもしれません。
Rの演算子は、関数呼び出しを行うための近道に過ぎません。他の関数呼び出しとまったく同じように、クラスに基づいて、演算子の特殊な動作を自由に定義することができます。その間に、最初のシステムの他の弱点も修正することができます。
- 必要に応じて任意の数の無限ベクトルを生成できる方がよい。
- ランダム分布の使用を構成できる方がよい。
- 別のベクトルの値で無限乱数ベクトルを初期化するオプションがあるとよい。
では、やってみましょう。
リスト14.インデックス可能な無限乱数ベクトルの定義
"[.infinite_random" <- function(v, index) {
name <- attr(v, 'name')
rfunc <- attr(v, 'rfunc')
extend_by = max(index-length(v), 0)
extras = rfunc(extend_by)
new <- c(v, extras)
makeInfiniteRandomVector(name, v=new, rfunc)
return(new[index])
}
unitnorm <- function(n) return(rnorm(n,0,1))
empty <- vector('numeric', 0)
makeInfiniteRandomVector <- function(name, v=empty, rfunc=unitnorm) {
# Create an infinite vector
# optionally extend existing vector, configurable rand func
attr(v,'class') <- 'infinite_random'
attr(v,'name') <- name
attr(v,'rfunc') <- rfunc
assign(name, v, env=.GlobalEnv)
}
makeInfiniteRandomVector('v')
# makeInfiniteRandomVector('inf_poisson', rfunc=my_poisson)
# Usage is just, e.g.: v[1]; v[10]; v[9:12]; etc.
|
インデックス化は汎用関数としてRによってすでに定義されているので、UseMethod()を呼び出してセットアップする必要はありません。好きなだけ新しい特殊化を定義すればよいのです。同様に、組み込みのprint()関数も汎用関数です。これを次のように特殊化することができます。
リスト15.無限ベクトルのプリント
print.infinite_random <- function(v) {
a_few = 5
len = length(v)
end_range = (len-a_few)+1
cat('* Infinite Random Vector *\n')
cat('[1] ', v[1:a_few], '...\n')
cat('[')
cat(end_range)
cat('] ', v[end_range:len], '...\n')
}
|
実行すると、このコードは次のような出力を生成します。
リスト16.無限ベクトルのプリント例
"[.infinite_random" <- function(v, index) {
name <- attr(v, 'name')
rfunc <- attr(v, 'rfunc')
extend_by = max(index-length(v), 0)
extras = rfunc(extend_by)
new <- c(v, extras)
makeInfiniteRandomVector(name, v=new, rfunc)
return(new[index])
}
unitnorm <- function(n) return(rnorm(n,0,1))
empty <- vector('numeric', 0)
makeInfiniteRandomVector <- function(name, v=empty, rfunc=unitnorm) {
# Create an infinite vector
# optionally extend existing vector, configurable rand func
attr(v,'class') <- 'infinite_random'
attr(v,'name') <- name
attr(v,'rfunc') <- rfunc
assign(name, v, env=.GlobalEnv)
}
makeInfiniteRandomVector('v')
# makeInfiniteRandomVector('inf_poisson', rfunc=my_poisson)
# Usage is just, e.g.: v[1]; v[10]; v[9:12]; etc.
|
まとめ
Rで汎用の関数、オブジェクト、およびクラスをプログラミングするには、伝統的な手続き型およびオブジェクト指向プログラマーの思考方法を振り返る必要があります。以前の2回の記事では、アドホックな統計実験の例を示しましたので、そのような再考は不要でしたが、コードを再利用したい場合は、汎用関数と汎用関数で作成できる「裏返し」のOOPの概念を理解することが重要です(「裏返し」形式は、実際、より汎用性が高い形式です)。
OOP全体を「どんなコードが呼び出されるか」と「どのようにして決定が下されるか」という問題で考えるのがコツです。C++、Objective C、Java、Ruby、Pythonなど、表現に使用する特定の構文言語にこだわるのではなく、ディスパッチそのものの概念に注目してください。
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | David Mertz氏は多くの分野で活躍しています。ソフトウェア開発や、それについて著述もしています。その他、学術政策理念について分野を問わず、関係する雑誌に記事も書いています。かなり以前には、超限集合論、ロジック、モデル理論などを研究していました。その後、労働組合組織者として活動していました。そして、David Mertz氏自身は人生の半ばにもまだ達していないと思っているので、これから何かほかの仕事をするかもしれません。 |
記事の評価
|  |