Rにおける遅延評価や環境,サーチパスのこと

遡ること数時間前...関数のリスト作ろうとしたら妙なことが起こった - もうカツ丼でいいよな
twitterでつぶやいたらいろいろな方が反応してくれたのでちょっと検索した.


え?いちねんまえ?

サーセン><出直してきます!
というわけで家帰って本とか調べました.
結論を言うと,遅延評価もそうなんだけど評価される時のスコープの問題だった.サーチパス上にちゃんと評価されたオブジェクトがあるか否か.
やっとクロージャだとかレキシカルスコープだとかいう単語の意味が分かったような気がする.気がするだけかもしれないので間違ってたら教えて下さい.
以下分かったことまとめ.やたら長くなりました.


ポイント

  • forは関数であるということ
  • 関数は環境(environment)を生むということ
  • 環境はサーチパスに従って検索されるということ
  • 関数の実引数にオブジェクトでなく式が与えられた場合,それは遅延評価の対象となるということ
  • そこで指定された式はその引数が呼び出されたときに初めて評価されるということ

関数呼び出しにおける遅延評価

Rでは,関数の実引数として式が与えられた場合それは遅延評価の対象となる.いつ評価されるかと言えば,それは関数内部でその引数が呼び出された時.

環境とサーチパス

オブジェクトが呼ばれたとき,オブジェクトは現在利用可能な環境の中から探される.このとき,環境を探す順番をサーチパスと呼ぶ.サーチパスはsearch()で確認することができる.

> search()
 [1] ".GlobalEnv"        "package:stats"     "package:graphics" 
 [4] "package:grDevices" "package:utils"     "package:datasets" 
 [7] "JapanEnv"          "package:methods"   "Autoloads"        
[10] "package:base"  

どのようなパッケージを読み込んでいるかにより多少異なるが大体このようなものが表示されると思う.
ここで,".GlobalEnv"はサーチパスの"0番目"に位置しており,最後の"package:base"は"-10番目"に位置している.
オブジェクトが探索される順番はサーチパスの並びで番号の大きい側から,つまりここでは".GlobalEnv"から"package:base"の方向となる.
環境はパッケージを読み込んだり,関数を呼び出したりすると(後述)生成され,サーチパスに追加される.

環境の生成

関数を呼び出すと暗黙の内に環境が生成される(環境 -R Language Definition).そして環境はふつう関数の実行終了とともに消去される(ただし後に述べるように例外がある).このような仕組みのおかげで他の環境の変数名と関数中の変数名が衝突する心配をしなくて済む.
関数呼び出しにより生成された環境は,サーチパスの先頭に登録される.すなわち,".GlobalEnv"の前,サーチパスの"1番目"である.もしその関数中で関数が呼び出された場合,その関数により生成された環境はサーチパスの"2番目"に登録される.
また,このようにある環境中で関数オブジェクトが生成された場合,その関数オブジェクトは自身の環境内のオブジェクトだけではなく,自身を生み出した"親"環境のオブジェクトも参照できる.これはオブジェクトの探索がサーチパスの番号の大きい側から行われるということと同じことを言っている.そして,サーチはサーチパスの番号が増加する方向へは行われないということだ.このようなスコープを静的スコープ,またはレキシカルスコープと呼ぶ.
そして引数以外の変数について,関数実行時の環境を起点に変数をサーチするのではなく,"その関数の環境"のレキシカルスコープにおいて解決する関数のことをクロージャと呼ぶ.

for

forは内部的には関数である(流れ制御要素 -R Language Definition).次の例を見れば分かるだろう.

> for(i in 1:3) print(i)
[1] 1
[1] 2
[1] 3
> "for"(i, 1:3, print(i))
[1] 1
[1] 2
[1] 3

つまり,forも環境を作り出す.

結局何が起こっていたのか

結局のところ関数のリスト作ろうとしたら妙なことが起こった - もうカツ丼でいいよなでは何が起こっていたのか.
見通しを良くするために関数のリスト作ろうとしたら妙なことが起こった - もうカツ丼でいいよなでlapplyを使っていた部分をforで書き直す.

para1 <- c(  1,  3,  5)
para2 <- c(  7, 11, 13)
pa <- data.frame(para1, para2)

## case 1'
fun3t <- function(x){
  function(){
    x[1] + x[2] + x[3]
  }
}
fun3 <- list(0)
f <- function()
for(i in 1:2){
  fun3[[i]] <- fun3t(pa[[i]])
}
## case 2'
fun4t <- function(x){
  x
  function(){
    x[1] + x[2] + x[3]
  }
}
fun4 <- list(0)
for(i in 1:2){
  fun4[[i]] <- fun4t(pa[[i]])
}

実行結果はこのようになる.

> fun3[[1]]()
[1] 31
> fun3[[2]]()
[1] 31
> fun4[[1]]()
[1] 9
> fun4[[2]]()
[1] 31

fun4の挙動が欲しい.fun3は異なるパラメータを渡しているつもりなのに,最後のパラメータだけが採用されてしまっている.
2つのリストの中身はこうなっていて,一見すると同じに見える.

> fun3
[[1]]
function () 
{
    x[1] + x[2] + x[3]
}
<environment: 0x0380136c>

[[2]]
function () 
{
    x[1] + x[2] + x[3]
}
<environment: 0x03801120>

> fun4
[[1]]
function () 
{
    x[1] + x[2] + x[3]
}
<environment: 0x0400405c>

[[2]]
function () 
{
    x[1] + x[2] + x[3]
}
<environment: 0x04002030>

しかしenvironmentが.GlobalEnvではないという点に注意しなければいけない.関数が別の環境に関数オブジェクトを返り値として与える場合,関数の環境は消去されずに残り,返り値として出力された関数には専用の環境が与えられる.
つまり,上記リスト中のxは全て同じ変数名を使っているが,環境が異なるために指すオブジェクトは必ずしも同じではないということである.
問題は,この変数名がどのオブジェクトを指しているかという点にある."親"の方向へ辿っていったときに,どんなオブジェクトに出会うのか.
まず,fun3のリストをどのように作成したかもう一度見てみよう.

## case 1'
fun3t <- function(x){
  function(){
    x[1] + x[2] + x[3]
  }
}
fun3 <- list(0)
for(i in 1:2){
  fun3[[i]] <- fun3t(pa[[i]])
}

functionや代入演算子も関数なので環境が発生するが,それらはとりあえず消滅するので問題ではない.問題はforに差し掛かったときだ.多少簡略化するけどこんな感じで環境が作られているハズだ.

  1. forの環境が生成される.変数iがforの環境に保持される.
  2. fun3tの環境が生成される.paiが渡されるが,内部で引数が呼び出されないので評価されない.paiとして保持される.
  3. fun3tの中の無名関数の環境が生成される.

最後に生成された無名関数がリストの要素に代入される訳だが,その無名関数の中の"引数ではない変数"xがどのように解決されるのかがポイントとなる.fun31()もしくはfun32()を実行したとしよう.

  1. 無名関数が生成した環境中から探す.見つからない.
  2. fun3tが生成した環境中から探す.paiが渡されている.しかしiが評価されていないのでさらにiを探す必要がある.
  3. forが生成した環境中から探す.ループが終了して2が代入されているiが見つかる.

iが2だから,fun31()を呼び出そうがfun32()を呼び出そうがfun3tにはpa2が渡され,実行結果はどちらも同じになる.
ではfun4のリストの場合を見てみる.

## case 2'
fun4t <- function(x){
  x
  function(){
    x[1] + x[2] + x[3]
  }
}
fun4 <- list(0)
for(i in 1:2){
  fun4[[i]] <- fun4t(pa[[i]])
}

ここでのforの流れはこんな感じになる.

  1. forの環境が生成される.変数iがforの環境に保持される.
  2. fun4tの環境が生成される.fun4t中でxが呼び出されているので,fun4tに渡されたpaiが評価され,pa1またはpa2として保持される.
  3. 無名関数の環境が生成される.

ここでfun41()またはfun42()を呼び出す.

  1. 無名関数が生成した環境中から探す.見つからない.
  2. fun3tが生成した環境中から探す.paiが渡されているが,評価されているのでpa1もしくはpa2になっている.

fun41()ならpa1が,fun42()ならpa2がちゃんと渡される.