キマイラ・サイトは http://www.chimaira.org/です。
トラックバック/コメントは日付を気にせずにどうぞ。
連絡は hiyama{at}chimaira{dot}org へ。
蒸し返し歓迎!
ところで、アーカイブってけっこう便利ですよ。タクソノミーも作成中。今は疲れるので作っていません。
2015-07-14 (火)
R言語メタプログラミングの基礎
雑記/備忘 | |
Rはメタプログラミングの能力を持っていますが、情報がまとまってなくて苦労します。けっこう落とし穴もあります。基本的な事項をここにまとめておきます。
内容:
- 関数オブジェクトの基本構造
- 関数オブジェクトのコンストラクタ
- ペアリストと仮引数リスト
- 空な名前とデフォルト値なし
- コールオブジェクトと関数本体
- 関数の評価環境
- 関数の登録先環境
- コールオブジェクトの操作
- 引数の式をコールオブジェクトとして取得する
- その他のメタプログラミング・ツール
関数オブジェクトの基本構造
ユーザーが定義した関数をデータとして見ると、3つの部位(スロット、メンバー)を持つ構造体と考えることができます。3つの部位とは、仮引数リスト(formal parameter list)、本体(body)、環境(environment)です。その要点を次の表にまとめます。表内のfは関数オブジェクトです。
関数の部位 | 型(モード) | 参照方法 | 変更方法 |
---|---|---|---|
仮引数リスト | pairlist | formals(f) | formals(f) <- my.formals |
本体 | call | body(f) | body(f) <- my.body |
環境 | environment | environment(f) | environment(f) <- my.envir |
以下にRコンソールを引用しますが、入力と出力の色を変えることはしていません(面倒だから)。プロンプト記号「> 」をたよりに入出力を区別してください。
3つの部位を取り出す関数 formals(), body(), environment() の使用例を示します。
> f <- function(x, y = 0, ...) {x + y + 1} > f function(x, y = 0, ...) {x + y + 1} > formals(f) $x $y [1] 0 $... > mode(formals(f)) [1] "pairlist" > body(f) { x + y + 1 } > mode(body(f)) [1] "call" > environment(f) <environment: R_GlobalEnv> > mode(environment(f)) [1] "environment" >
組み込み関数の場合は、関数の各部を取り出すことは出来ません。
> c function (..., recursive = FALSE) .Primitive("c") > mode(c) [1] "function" > formals(c) NULL > body(c) NULL > environment(c) NULL >
3つの部位を変更(更新)する例は、次の節のコンストラクタ・コード内に出てきます。
関数オブジェクトのコンストラクタ
関数オブジェクトのコンストラクタは`function`関数だと思うのですが、呼び出し方がわかりません。
> `function` .Primitive("function") > `function`() エラー: "function" への引数の個数が正しくありません > `function`(pairlist(x = 1)) エラー: "function" への引数の個数が正しくありません > `function`(pairlist(x = 1), quote(1)) エラー: "function" の仮引数リストが不正です >
しょうがないので、関数オブジェクトのコンストラクタを自作することにします。
makeFunction <- function(fpars, fbody, fenvir = .GlobalEnv) { f <- function(){} formals(f) <- fpars body(f) <- fbody environment(f) <- fenvir f }
まず、“仮引数リストも本体もカラッポの関数”を作っておいて、各部を変更して目的の関数を作ります。関数の各部位(仮引数リスト、本体、環境)のデータ構造についてはこの後で述べます。makeFunction()の使用例は次のとおり。
> addOne <- makeFunction(pairlist(n = 0), quote(n + 1)) > addOne function (n = 0) n + 1 > addOne() [1] 1 > addOne(3) [1] 4 >
ペアリストと仮引数リスト
関数の仮引数リストは、ペアリスト(pairlist)と呼ばれるデータ型(モード)を持ちます。ペアリストは、現在のRではほとんど表層に現れないデータ型ですが、なぜか仮引数リストはペアリストとなっています。他のペアリストの例としては.Options変数があります。
> mode(formals(f)) [1] "pairlist" > mode(.Options) [1] "pairlist" >
ペアリストの操作方法はリストとまったく同じです。以下に、ペアリストの生成、長さの取得、成分(要素)の参照と変更・追加、名前(要素のラベル)ベクトルへのアクセスと変更、の方法を示します。
> # 生成 > pl <- pairlist(x = 0, 1) > pl $x [1] 0 [[2]] [1] 1 > # 長さ > length(pl) [1] 2 > # 成分へのアクセス > pl[[1]] [1] 0 > pl[[2]] [1] 1 > pl$x [1] 0 > # 成分の変更 > pl$x <- 1 > pl[[2]] <- 10 > pl $x [1] 1 [[2]] [1] 10 > # 成分の追加 > pl[[3]] <- 100 > pl $x [1] 1 [[2]] [1] 10 [[3]] [1] 100 > # 名前ベクトルへのアクセスと変更 > names(pl) [1] "x" "" "" > names(pl) <- c("a", "b", "c") > pl $a [1] 1 $b [1] 10 $c [1] 100 >
関数の仮引数リストをペアリストで表現するときは:
- ペアリストの長さが引数の個数(アリティ)となる。
- 引数の名前を名前ベクトルとして指定する。
- 引数のデフォルト値をペアリストの成分(要素)として指定する。
例えば、x, y という名前の2つの引数があり、デフォルト値がそれぞれ 1, 2 である仮引数リストは、pairlist(x = 1, y = 2) として作成できます。特殊な引数名である ...(ドット3つ)も通常の引数名と同じ扱いです。「デフォルト値なし」をどうするかは次節で説明します。
次の関数は、仮引数リスト(fplistと略称している)を簡単に作る関数です。引数の名前は、a, b, c, ... と自動的に(あるいは勝手に)付けます。あまり実用的ではありませんが、仮引数リストの動的生成の参考にはなるでしょう。
makeFPList <- function( # 仮引数リストの長さ n = 1, # 仮引数リストのデフォルト値のリスト # 同じモードの値の並びなら、リストではなくてベクトルでもよい val = list(NULL) ) { # nの範囲チェック stopifnot(0 <= n, n <= length(letters)) if (n == 0) { # 例外的なので先に処理してしまう return(NULL) } fplist <- pairlist(val) # pairlist() は、NULLとなるので初期値に使えない val <- rep_len(val, n) # 長さがnになるように繰り返しておく for (i in 1:n) { # fplist[[i]] <- val はダメ、成分が削除されることがある fplist[i] <- pairlist(val[[i]]) } names(fplist) <- letters[1:n] # 引数名ベクトルをセットする fplist }
上記コードのコメントに「fplist[[i]] <- val はダメ、成分が削除されることがある」と書いてありますが、これについては「NULLと「存在しない」は違うんだってば!」を参照ください。
空な名前とデフォルト値なし
トップレベル(インタプリタ)に対して名前abcを直接入力すると、それは評価されます。名前をクォート(quote()関数を適用)すると、データ型(Rではモードと呼ぶ)がnameであるオブジェクトが返ります。つまり、quote(abc) は名前データということになります。
> abc エラー: オブジェクト 'abc' がありません > quote(abc) abc > mode(quote(abc)) [1] "name" > my.name <- quote(abc) > my.name abc >
名前データを作るには、as.name()関数が使えます。
> as.name("abc") abc > mode(as.name("abc")) [1] "name" >
しかし、空な名前は作れません。
> as.name("") 以下にエラー as.name("") : 長さ 0 の変数名を使おうとしました >
ユーザーレベルで空な名前はどうやっても作れないようです。しかし、空な名前が値として返ってくる状況はあります。関数の仮引数リストにおいて、「デフォルト値がないときの値」として空な名前が使われているのです。
> f <- function(x) {} > formals(f) $x > formals(f)[[1]] > mode(formals(f)[[1]]) [1] "name" > as.character(formals(f)[[1]]) [1] "" >
仮引数リストを作るとき、「デフォルト値がないときの値」として空な名前が必要なので、空な名前を返す関数を定義しておきます。
noDefault <- function() { f <- function(x){} formals(f)[[1]]; }
noDefault()を、空な名前を表すリテラルのように使えますが、その値を代入(束縛)した変数は参照できなくなるので注意してください。
> noDefault() > empty <- noDefault() > empty エラー: 引数 "empty" がありませんし、省略時既定値もありません >
noDefault()の値は、変数に経由せずに直接受け渡す必要があります。noDefault()を使って、デフォルト値なし仮引数リストを作ることができます。
> pairlist(x = noDefault(), y=noDefault()) $x $y > makeFunction(pairlist(x = noDefault(), y = noDefault()), quote(x + y)) function (x, y) x + y > f <- makeFunction(pairlist(x = noDefault(), y = noDefault()), quote(x + y)) > f function (x, y) x + y > f(2, 3) [1] 5 > f(2) 以下にエラー f(2) : 引数 "y" がありませんし、省略時既定値もありません > f() 以下にエラー f() : 引数 "x" がありませんし、省略時既定値もありません >
コールオブジェクトと関数本体
関数の本体はcallというデータ型(モード)のデータです。コールオブジェクトは、Rの式をデータとみなしたものです。コールオブジェクトを作成する最も簡単な方法はquote()関数を使うことです。
> quote(1 + 2) 1 + 2 > mode(quote(1 + 2)) [1] "call" > quote(x + y * 2) x + y * 2 > mode(quote(x + y * 2)) [1] "call" >
コールオブジェクトはデータなので変数に代入(束縛)できます。
> e1 <- quote(1 + 2) > e2 <- quote(x + y * 2) > e1 1 + 2 > e2 x + y * 2 >
コールオブジェクトを評価するには、eval()関数を使います。
> eval(e1) [1] 3 > eval(e2) 以下にエラー eval(expr, envir, enclos) : オブジェクト 'x' がありません > x <- 2 > eval(e2) 以下にエラー eval(expr, envir, enclos) : オブジェクト 'y' がありません > y <- 3 > eval(e2) [1] 8 >
変数を含むコールオブジェクトを評価するには、変数名と値の束縛を保持する評価環境が必要です。デフォルトでは、eval()が呼ばれたときの環境が使われますが、eval()の第2引数に他の環境や束縛を表すデータを渡せます。
> eval(e2) [1] 8 > eval(e2, list(x = 1)) [1] 7 > eval(e2, list(x = 1, y = 2)) [1] 5 > env <- new.env() > env$x <- 5 > env$y <- 10 > eval(e2, env) [1] 25 >
関数の本体にはコールオブジェクトを指定します。関数を作る際に、本体が前もって分かっている(静的な式)なら、quote()で本体を記述できます。
> f <- makeFunction(pairlist(x = noDefault(), y = noDefault()), quote(x + y * 2)) > f function (x, y) x + y * 2 > f(2, 3) [1] 8 > f(1, 3) [1] 7 > f(1, 2) [1] 5 >
関数本体を動的に構築する方法は後の節で述べます。
関数の評価環境
関数に引数変数以外の変数が登場するとき、その変数の値は環境から供給する必要があります。関数本体の評価環境は関数の定義時に指定できます。
> env <- new.env() > env$a <- 5 > f <- makeFunction(pairlist(x = noDefault()), quote(x + a), env) > f function (x) x + a <environment: 0x00000000097a7560> > f(1) [1] 6 > f(2) [1] 7 > env$a <- 1 > f(1) [1] 2 > f(2) [1] 3 >
この例では、新しい環境(環境もRのデータです)を生成して、その環境におけるaの値を5にセットします。そして、関数fには今作った環境をセットします。関数本体である式 x + a は a = 5 という束縛のもとで評価されるので、f(1) なら 1 + 5 が計算されます。関数の評価環境の内容を外から変更することもできます。
Rのインタプリタ・トップレベルである大域環境は、.GlobalEnvという名前を持っています。ブラウザのJavaScriptにおけるwindowオブジェクトのようなものです。makeFunction() では、明示的に環境を指定しなければ、デフォルトとして大域環境.GlobalEnvが使われます。
関数の登録先環境
関数makeFunction()は関数を作って返しますが、できた関数は無名のオブジェクトであり名前を持っていません。名前を持たせるには、関数オブジェクトを変数に代入します。代入とは、環境に「名前とオブジェクトの束縛」を追加することです。したがって、名前を与えるときは名前の登録先である環境を指定する必要があります。
以下の関数defineFunction()は、第1引数に名前(の文字列)、第5引数に登録先の環境を(必要なら)指定して名前付き関数を定義します。
defineFunction <- function(fname, fpars, fbody, fenvir = .GlobalEnv, target = .GlobalEnv) { f <- makeFunction(fpars, fbody, fenvir) assign(fname, f, envir = target) f }
次はdefineFunction()の使用例です。
> defineFunction("addOne", pairlist(x = noDefault()), quote(x + 1)) function (x) x + 1 > addOne(3) [1] 4 > env <- new.env() > defineFunction("succ", pairlist(x = noDefault()), quote(x + a), env, env) function (x) x + a <environment: 0x000000000975c7e8> > env$succ(3) 以下にエラー env$succ(3) : オブジェクト 'a' がありません > env$a <- 1 > env$succ(3) [1] 4 > env$a <- 2 > env$succ(3) [1] 5 >
コールオブジェクトの操作
コールオブジェクトはリストと同じように扱えます。コールオブジェクト内に、Rの式を構文解析したツリーがリスト形式で保存されていると思えばいいのです。ツリーの末端(リーフノード)は数値や文字列のようなアトミックデータです。ツリーの中間のノードは関数となり、子ノードに引数がぶら下がります。
例として、x + y * 2 を見てみましょう。
> e <- quote(x + y * 2) > e[[1]] `+` > e[[2]] x > e[[3]] y * 2 > e[[3]][[1]] `*` > e[[3]][[2]] y > e[[3]][[3]] [1] 2 >
分かりますか? Rの内部形式では、中置演算子も関数呼び出しの形で保存されます。x + y * 2 は、`+`(x, `*`(y, 2)) です。そして次のルールでリストが形成されます。ここで、`+`などは名前(シンボル)です。予約語や特殊文字を含む名前はバッククォートで囲んで特別な意味をエスケープします。
- 関数はリストの第1成分となる。
- 第1引数は、リストの第2成分となる。
- 第2引数は、リストの第3成分となる。
- 以下同様。
`+`(x, `*`(y, 2)) は、次のリストと同じ構造になります。
- list(`+`, quote(x), list(`*`, quote(y), 2))
ただし、コールオブジェクトはリストそのものではないので、リストからコールオブジェクトへの変換には、as.call()関数を使う必要があります(その例は後述)。
既にあるコールオブジェクトを変更することもできます。x + y * 2 の`+`を`-`に、yをzに置き換えてみます。
> e <- quote(x + y * 2) > e[[1]] `+` > e[[1]] <- `-` > e .Primitive("-")(x, y * 2) > e[[2]] x > e[[2]] <- quote(z) > e .Primitive("-")(z, y * 2) > eval(e, list(x = 3, z = 10)) [1] 4 >
コールオブジェクトをプログラムで白紙から作るとときは、いったんリストを作って、それをas.call()関数でコールに変換するといいでしょう。as.call()は入れ子の変換はしてくれないので注意してください。
> x [1] 2 > y [1] 3 > eval( as.call(list(`+`, quote(x), list(`*`, quote(y), 2))) ) 以下にエラー .Primitive("+")(x, list(.Primitive("*"), y, 2)) : 二項演算子の引数が数値ではありません > eval( as.call(list(`+`, quote(x), as.call(list(`*`, quote(y), 2))) ) ) [1] 8 >
call()関数でコールオブジェクトを作ることもできます。call()の第一引数は関数名を表す文字列です。文字列の代わりに名前(シンボル)を渡すとエラーのようです。
> call("+", 1, 2) 1 + 2 > call(`+`, 1, 2) 以下にエラー call(`+`, 1, 2) : 最初の引数は文字列でなくてはなりません > e <- call("+", 1, 2) > e[[1]] `+` > mode(e[[1]]) [1] "name" > e[[2]] [1] 1 > mode(e[[2]]) [1] "numeric" >
Rのコールオブジェクトで驚いたことがあります。Rでは、丸括弧や波括弧も構文解析ツリーのそのまま保持されます。しかも、関数としてです。
> e <- quote( (x + y)*2 ) > e (x + y) * 2 > e[[1]] `*` > e[[2]] (x + y) > e[[2]][[1]] `(` >
`(`や`{`は関数名なのです。これらは構文上のまとまりを付ける目的なので、処理としては恒等関数と同じになります。
引数の式をコールオブジェクトとして取得する
Rは遅延評価を行うので、関数が実行された段階で、その引数の式はまだ評価されていません。そのため、関数の中から引数に渡された式(値ではない!)を取得することができます。引数の式をコールオブジェクトとして取り出すには、substitute()関数を使います。
exp_val <- function(x) { exp <- substitute(x) val <- x print(exp) print(val) }
exp <- substitute(x) の代わりに exp <- quote(x) とはできません。quote()を使うと、expには式としてのxが代入されてしまいます。
exp_val()の実行例:
> exp_val(1 + 2) 1 + 2 [1] 3 > x [1] 2 > y [1] 3 > exp_val(x * y) x * y [1] 6 >
その他のメタプログラミング・ツール
expressionというデータ型もあります。エクスプレッションは、コールオブジェクトのリストです。expression(1 + 2, x + y * 2) は、list(quote(1 + 2), quote(x + y * 2)) とほぼ同じです。エクスプレッションがリストと違うのは、eval()に渡すと評価されることです。最後の式(コールオブジェクト)の評価結果が全体の値となります。
> ex <- expression(1 + 2, x + y * 2) > mode(ex) [1] "expression" > ex[[1]] 1 + 2 > ex[[2]] x + y * 2 > x [1] 2 > y [1] 3 > eval(ex) [1] 8 > eval(ex[[1]]) [1] 3 >
エクスプレッションを入れ子にすると、内側のエクスプレッションはコールオブジェクトと解釈されます。
> ex <- expression(1 + 2, expression(x + y * 2, y^2)) > ex[[1]] 1 + 2 > ex[[2]] expression(x + y * 2, y^2) > mode(ex[[2]]) [1] "call" > eval(ex[[2]]) expression(x + y * 2, y^2) > eval(ex) expression(x + y * 2, y^2) > y [1] 3 > eval(eval(ex)) [1] 9 >
チルダを含む式は、quote()に入れなくてもそのままでコールオブジェクトとみなされます。これは、フォーミュラと呼ばれるデータで、Rのなかでは簡単なDSL(Domain Specific Language)として使われています。
> x ~ y x ~ y > mode(x ~ y) [1] "call" > class(x ~ y) [1] "formula" > quote(x ~ y) x ~ y > mode(quote(x ~ y)) [1] "call" > (x ~ y)[[1]] `~` > (x ~ y)[[2]] x > (x ~ y)[[3]] y >
関数を呼び出すメタ関数としてdo.call()があります。第1引数は、呼び出すべき関数そのものか、関数の名前(の文字列)です。第2引数に引数リストを渡します。
> do.call(function(x, y){x + y^2}, list(3, 4)) [1] 19 > do.call(function(x, y){x + y^2}, list(y = 3, x = 4)) [1] 13 > f <- function(x, y){x + y^2} > do.call(f, list(y = 3, x = 4)) [1] 13 > `f` function(x, y){x + y^2} > do.call(`f`, list(y = 3, x = 4)) [1] 13 > do.call("f", list(y = 3, x = 4)) [1] 13 >
本文内に登場したユーティリティ関数をまとめておきます。
# デフォルト値なしを表す空な名前を返す noDefault <- function() { f <- function(x){} formals(f)[[1]] } # 指定された仮引数リスト、本体、環境から関数を作成する makeFunction <- function(fpars, fbody, fenvir = .GlobalEnv) { f <- function(){} formals(f) <- fpars body(f) <- fbody environment(f) <- fenvir f } # 関数を作成して、指定された名前で目的の環境に登録する defineFunction <- function(fname, fpars, fbody, fenvir = .GlobalEnv, target = .GlobalEnv) { f <- makeFunction(fpars, fbody, fenvir) assign(fname, f, envir = target) f } # 仮引数リストを作る # 引数名は、アルファベットの文字を順番に使う。 makeFPList <- function( # 仮引数リストの長さ n = 1, # 仮引数リストのデフォルト値のリスト # 同じモードの値の並びなら、リストではなくてベクトルでもよい val = list(NULL) ) { # nの範囲チェック stopifnot(0 <= n, n <= length(letters)) if (n == 0) { # 例外的なので先に処理してしまう return(NULL) } fplist <- pairlist(val) # pairlist() は、NULLとなるので初期値に使えない val <- rep_len(val, n) # 長さがnになるように繰り返しておく for (i in 1:n) { # fplist[[i]] <- val はダメ、成分が削除されることがある fplist[i] <- pairlist(val[[i]]) } names(fplist) <- letters[1:n] # 引数名ベクトルをセットする fplist }
- 52 https://www.google.co.jp/
- 13 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0CB8QFjAAahUKEwiH7K71vNnGAhUk2KYKHQWFAC4&url=http://d.hatena.ne.jp/m-hiyama/20100603/1275546996&ei=m2akVceyJKSwmwWFioLwAg&usg=AFQjCNFT_9wyYE1cfV9qmsiO6k_u7JWvAQ&bvm=bv.976530
- 9 http://www.google.co.jp/url?url=http://d.hatena.ne.jp/m-hiyama/20140203/1391381365&rct=j&frm=1&q=&esrc=s&sa=U&ved=0CB0QFjABahUKEwjXnduNvdnGAhXDrKYKHctmAGc&usg=AFQjCNE6EN8AtmQ3kpvkuH45pi-3OaxEow
- 7 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=3&ved=0CDAQFjACahUKEwitx6z9v9nGAhUjIaYKHSSDCjU&url=http://d.hatena.ne.jp/m-hiyama/20140203/1391381365&ei=0WmkVe2aJ6PCmAWkhqqoAw&usg=AFQjCNH9d78u7pFo60oVRnz2wboInB1-pw&bvm=bv.976530
- 6 http://b.hatena.ne.jp/
- 6 http://pipes.yahoo.com/pipes/pipe.info?_id=3eebace824bb60a10f13c841c2c64478
- 6 http://reader.livedoor.com/reader/
- 5 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&frm=1&source=web&cd=8&cad=rja&uact=8&ved=0CEkQFjAHahUKEwiJ6NSIv9nGAhUFXqYKHX-iDgA&url=http://d.hatena.ne.jp/m-hiyama/20071214/1197598785&ei=3GikVYmKOYW8mQX_xDo&usg=AFQjCNGRsI4Ej9bRRG81nQBIUThdct
- 5 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=2&ved=0CCUQFjABahUKEwiEq6uJvdnGAhXIIKYKHQYqCT4&url=http://d.hatena.ne.jp/m-hiyama/20140203/1391381365&ei=xWakVcS0HcjBmAWG1KTwAw&usg=AFQjCNH9d78u7pFo60oVRnz2wboInB1-pw&bvm=bv.976530
- 4 http://www.google.co.uk/url?sa=t&source=web&cd=1