Rのメタプログラミングとモナド (R Advent Calendar 2012)

R Advent Calendar 2012 : ATND の10日目の記事。

R言語は実行時に式や関数などの言語オブジェクトを組み立てたり加工したりする、いわゆるメタプログラミングのための機能が豊富に用意されている。本記事はそのクックブック的なまとめと、メタプログラミングの応用として「モナドをRで実装する」というのをやってみる。

ちなみにRのメタプログラミング@tyatsuta 氏主催のTokyoLangR勉強会にて色々と情報共有が行われている。特に大仏様とあらびき氏の資料が理解を深めるのにオススメ。

本記事、ちょっと長いので目次を。

モナドソースコードこちら

では早速。

【予備知識編その1】動的に関数を作る

パターン1. 文字列からparseする
> ex <- parse(text="function(x,y) x+y")
> ex
expression(function(x,y) x+y)
> eval(ex)(1,2)
[1] 3

関数定義のソースコード(文字列)をparseに与えると関数定義の「式(expression)」が返されるので、それをevalすることで関数オブジェクトが得られる。parseが返すのは「関数定義の式」なので、関数の実体を得るにはそれをevalする必要があることに注意。

このパターンは文字列処理を使って関数定義のソースコードを動的に生成するようなケースで使える。

パターン2. 引数と本体を個別に与える
> f <- function(){} # とりあえずダミー
> f
function(){}
> formals(f) <- alist(x=1,y=)
> f
function (x = 1, y) 
{
}

引数を付け替えるには、左辺値関数formalsで引数リストをalistで与えればよい。*1

続いて本体の付け替え。

> body(f) <- quote(print(paste(x,y)))
> f
function (x = 1, y) 
print(paste(x, y))

式をそのまま書くと評価されてしまうので、quoteで包んだものを与える*2

本体が複数の文になるときも同様に

> body(f) <- quote({ print(x); print(y); })
> f
function (x = 1, y) 
{
    print(x)
    print(y)
}

とquoteで囲めばOK。

このパターンは引数に与える変数の種類や関数本体の処理がそれぞれ予め想定できる場合に使える。

パターン3. quoteでなくcallを使う方法

例えば、x,yを取る二引数関数の本体を1/2の確率で足し算と掛け算のいずれかを選びたいとする。

> f<-function(x,y){} # 二引数関数のひな形
> f
function(x,y){}
> add<-function(x,y) x+y
> mul<-function(x,y) x*y
> op <- if (runif(1)>0.5) add else mul
> op
function(x,y) x*y
> body(f) <- quote(op(x,y))
> f
function (x, y) 
op(x, y)
> f(2,3)
[1] 6

という感じで関数を変数で切り替えればOKだが、この方法はちょっと問題があって以下のように後からopを変更すると(当然ながら)fの挙動も変わってしまう。

> op <- add
> f(2,3)
[1] 5

もちろん以下のように

> if (runif(1)>0.5) { body(f)<-quote(add(x,y)) } else { body(f)<-quote(mul(x,y)) }
> f
function (x, y) 
mul(x, y)

とすればopを介さずに本体を固定できるが、引数の(x,y)が共通なのが冗長な感じがする。
そこでquoteを使わずにcallを使うと、

> op <- if (runif(1)>0.5) "add" else "mul"
> body(f) <- call(op, quote(x), quote(y))
> f
function (x, y) 
mul(x, y)

という風にbodyを変更するタイミングで呼び出す関数を固定できる。callには関数の名前(=文字列)と、引数をquoteしたものを与えることに注意。

このようにcallを使うと、関数を動的に選びながら関数を固定して関数呼び出しの式を作ることができる。

パターン4. 関数の引数を動的に変更する

formalsで返されるalistは普通のリストのように要素の変更、削除(NULLを代入すればよい)、appendなどが出来る。

引数の追加

> f <- function(x,y=1) { print(x); print(y); }
> f
function(x,y=1) { print(x); print(y); }
> p<-formals(f)
> formals(f)<-append(p,alist(z=))
> f
function (x, y = 1, z) 
{
    print(x)
    print(y)
}
> body(f) <- quote({ print(x); print(y); print(z); })
> f
function (x, y = 1, z) 
{
    print(x)
    print(y)
    print(z)
}
> f(0,z=2)
[1] 0
[1] 1
[1] 2

alistで引数を与えるときに"z="という風にイコールをつける必要があることに注意。("z"という風にイコールをつけ忘れてもエラーを吐いてくれないので注意)

引数の削除

> f <- function(x,y=1) { print(x); print(y); }
> p <- formals(f)
> p[["y"]] <- NULL
> formals(f) <- p
> f
function (x) 
{
    print(x)
    print(y)
}
> f(1)
[1] 1
 以下にエラー print(y) :  オブジェクト 'y' がありません 
> y<-3
> f(1)
[1] 1
[1] 3

このパターンは引数の変数名(上の例だと"z")が事前に決まっているときに使えるが、変数名さえも実行時に決めたい場合は以下のように文字列をparseしてevalする以外方法がないようだ。

> params <- "x,y" # 動的に作成
> f <- eval(parse(text=paste("function(",params,"){}",sep="")))
> f
function(x,y){}
> # 以下 bodyを付け替えるなどする


パターン5. 関数本体の中身を動的に変更する

関数本体がブロックのとき、内部的には"{"という名前の可変長引数関数の呼び出し*3になっている。このことはR言語仕様書には詳しく書いてないが*4、以下のように調べると分かる。

> f <- function() { print("hello"); print("world"); }
> body(f)
{
    print("hello")
    print("world")
}
> class(body(f))
[1] "{"
> mode(body(f))
[1] "call"
> as.list(body(f))
[[1]]
`{`

[[2]]
print("hello")

[[3]]
print("world")
> f()
[1] "hello"
[1] "world"

関数呼び出しはリスト風にアクセスできるので、処理の追加や変更をしたい場合は以下のようにすればOK

> b <- body(f)
> length(b)
[1] 3
> b[[length(b)+1]] <- quote(print("!!"))
> body(f) <- b
> f
function () 
{
    print("hello")
    print("world")
    print("!!")
}
> f()
[1] "hello"
[1] "world"
[1] "!!"
> b[[2]] <- quote(print("Hello"))
> b
{
    print("Hello")
    print("world")
    print("!!")
}
> body(f) <- b
> f()
[1] "Hello"
[1] "world"
[1] "!!"

ただしリスト風にインデクスアクセスはできてもappendなどの通常のリスト演算は出来ないので、リストとして扱いたい場合は以下のようにいったんas.listしてas.callで戻す必要がある。

> b <- body(f)
> body(f) <- as.call(append(as.list(b),quote(print("hoge"))))
> f
function () 
{
    print("hello")
    print("world")
    print("!!")
    print("hoge")
}
> f()
[1] "hello"
[1] "world"
[1] "!!"
[1] "hoge"
> 


【予備知識編その2】値でなく式を扱う関数を作る

前節では式オブジェクトを色々いじってきたが、明示的に式オブジェクトの形でやりとりしなくてもRの関数呼び出しは式を参照できる仕組みを備えているのでいくつか例をご紹介。(と聞くと色々混乱してしまいそうになるが、R言語は関数の引数だけそういう特別扱いをするという仕様であり、それ以外の変数はみんな通常どおりなので心配は無用だ。遅延評価や式スロットの存在など特別なのは関数の引数だけ。)

テクニック1. 引数に与えられた式を受け取る

Rでは関数呼び出しのとき引数は遅延評価になり、呼び出された側の関数内で式を参照することができる。グラフを描くplot関数で凡例のところにグラフの式を表示できたり、フィッティングのlm関数で式を与えたりできるのはこの仕組みのおかげ。

> f <- function(x) { print(substitute(x)); print(x); }
> a <- 1
> b <- 2
> f(a+b)
a + b
[1] 3

呼び出し元が与えた式を取得するにはsubstitute関数を使う。

さらに、遅延評価によって評価された後でも、元の式は残っている。実は関数の引数ごとに「与えた式」と「評価した結果の値」の2つのスロットを持っている。前節にてformalsで関数の引数を変更するときにlistではなくalistを使ったのはそういう理由。
実際に、以下のようにprintしてからsubstituteしても

> f <- function(x) { print(x); print(substitute(x)); }
> a <- 1
> b <- 2
> f(a+b)
[1] 3
a + b

という風に元の式を取得できる。

テクニック2. ユーザ定義の演算子

Rではユーザが独自の2項演算子(2項のみ。単項や3項以上のユーザ定義演算子は不可)を定義できる。形はパーセント記号2つ"%%"で任意の文字列(ヌル文字列可)を挟んだもの。例えば、

> append(list(1),list(2,3))
[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] 3

> append(1,2)
[1] 1 2

というappendの仕様が気に入らないとき、次のような独自演算子"%++%"を定義することができる。

> "%++%" <- function(x,y) if (is.list(x) || is.list(y)) append(x,y) else list(x,y)
> list(1) %++% list(2,3)
[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] 3

> 1 %++% 2
[[1]]
[1] 1

[[2]]
[1] 2

という風に文字列"%++%"に直接functionを代入すれば2項演算子として機能してくれる。

【応用編】Rでモナド!

予備知識編のテクニックを使って、Rでモナドを実装してみた。とりあえずGistにソースをUpしてあるので興味のある方はどうぞ。

モナドとは何か

ここでは説明を省略しますが、モナドについて知りたいという方はこのサイトがかなりオススメです。

monad関数の使い方
  • monad関数にbindとreturnの関数を与えるとそれらを包んだdo構文のような書き方ができる関数(bindとretを包んだクロージャ)が返される。
  • do構文は、実際にはdo関数の呼び出しで、引数に"%<-%"演算子と"returm"(returnは予約語なので避けて最後"m"にしてある)を使って式を書く。
  • do関数を呼ぶと以下の仕様のクロージャが返される。
    • 内部で使うローカル変数は普通に使える。
    • 外側のスコープを参照する自由変数はdo関数を呼んだ側のスコープの変数を参照する。

具体例を次節で示す。

使用例1: Maybeモナド

ソースコード

maybe_do <- monad(
  bind = function(x,f) if(is.na(x)) NA else f(x),
  ret = function(x) x
)

maybe_test <- function() {

  loves_list <- list(
    c("Taro","Miki"),
    c("Jiro","Hanako"),
    c("Saburo","Hanako"),
    c("Daisuke","Youko"),
    c("Shunsuke","AnyGirl"),
    c("Masatoshi","AnyGirl"),
    c("Miki","Taro"),
    c("Hanako","Daisuke"),
    c("AnyGirl","Masao")
  )
  
  lover <- function(x) {
    for (p in loves_list) {
      if (x == p[1]) return(p[2])
    }
    return(NA)
  }

  do <- maybe_do

  couple_test <- function(a,b,lover) do (
    x %<-% lover(a),
    y %<-% lover(x),
    returm(x == b && y == a)
  )

  rival_test <- function(a,b,lover) do (
    x %<-% lover(a),
    y %<-% lover(b),
    returm(x == y)
  )

  jealousy_test <- function(a,b,lover) do (
    x %<-% lover(a),
    y %<-% lover(x),
    returm(y != a && y == b)
  )

  run <- function(test,a,b) {
    print(paste(substitute(test),a,"-->",b,":",test(a,b,lover)))
  }

  run(couple_test,"Taro","Miki")
  run(couple_test,"Jiro","Miki")
  run(couple_test,"Daisuke","Youko")
  run(rival_test,"Taro","Jiro")
  run(rival_test,"Jiro","Saburo")
  run(jealousy_test,"Shunsuke","Masao")
  run(jealousy_test,"Shunsuke","Taro")
  run(jealousy_test,"Jiro","Saburo")
}

maybe_test()

実行結果

[1] "couple_test Taro --> Miki : TRUE"
[1] "couple_test Jiro --> Miki : FALSE"
[1] "couple_test Daisuke --> Youko : NA"
[1] "rival_test Taro --> Jiro : FALSE"
[1] "rival_test Jiro --> Saburo : TRUE"
[1] "jealousy_test Shunsuke --> Masao : TRUE"
[1] "jealousy_test Shunsuke --> Taro : FALSE"
[1] "jealousy_test Jiro --> Saburo : FALSE"

ちゃんと動いている。NAかどうかをif文でいちいちチェックせずとも途中でNAになった瞬間にNAが返されるというMaybeモナドの売りがRで再現できた。めでたしめでたし。

使用例2: Listモナド

近日実装予定。

使用例3: Stateモナド

近日実装予定。

(余談)Rでモナドを実装してみて苦労した点
  • Rはレキシカルスコープなので、「関数を返す関数」をサプルーチン的に作るとスコープを外れてクロージャが作りづらい。
  • ちなみに動的に作った関数のスコープはevalしたときのレキシカルスコープになる。
  • do構文からモナド的なbindの呼び出しに変換する処理で再帰呼び出しを使わざるを得ず、動的に作る関数のレキシカルスコープがフラットになってしまう。
  • なのでbindとreturnをネストする形へ変換したあとdeparseしてparseしなおすことでフラットなレキシカルスコープを無理やり階層化するという実装になった。


おわりに

最後まで読んでいただきありがとうございました。しかしここまで読んでくれたあなたでさえ、メタプログラミングが何の役に立つのか?と思われたかもしれません。

私は以下のように考えています。

  • 関数や式を動的に作るテクニックにより、DSLDomain Specific Language: ドメイン特化言語)をR言語の上に構築できるようになります。
  • ちょうど、Rubyの上でRailsが動くように、言語の上に築かれたDSL(内部DSLという)はシステム開発の生産性を飛躍的に高める可能性があります。
    • 内部DSLの良い所は、ドメインに特化した言語でありながらそれを習得することが「汎用的なR言語の習得」にもなるぶんハードルが低くなることです。
  • データ分析の分野でも、Railsのように「毎回書くのが面倒なデフォルトの動作」を上手く規約として定義しておけると思います。
  • なので「データサイエンスにおけるDSL」を考える場合、「R上で動く内部DSL」のポテンシャルはかなり大きいと思います。
    • 例えば、モデル記述を変えること無くデータ量などの条件に応じて内部動作をダイナミックに切り替ながら動く予測システムなどが考えられます。

というわけで、Rに限らないかもしれませんがメタプログラミングは内部DSLの設計という意味で今後重要になっていくと思っています。

*1:listで無いことに注意。alistを使うのは、関数の引数は通常の変数と違って内部的に特別なpromiseという遅延評価で評価する変数だから。

*2:quoteすると式オブジェクトではなく名前オブジェクトまたは呼び出し(call)オブジェクトが返されるが、いずれも大体同じなので困らない限りはあまり気にしなくてOK。

*3:正確にはcallオブジェクト

*4:というか複数のcallオブジェクトを並べたexpressionだとかいうウソが書いてある。実際は"{"っていう名前の可変長引数関数のcallオブジェクトになってるじゃんか…