<<-とクロージャ

Posted on
R

R Advent Calendar 2021の21日目の記事です。

演算子<<-はグローバル環境に代入する、という説明を見かけることがあります。 例えばRの基礎とプログラミング技法には

演算子"<<-"は常に.GlobalEnv環境にオブジェクトを生成する.

とあります。

しかし常に、というのは正確ではないです。 今日は<<-の挙動とその使い道についてお話します。

<<-の挙動

では<<-の挙動は? R言語徹底解説には以下のように書かれています。

通常の付値演算子<-は現在の環境に変数を作成する. 一方<<-は現在の環境ではなく,親環境をさかのぼって最初に見つかった変数を修正する.


変数が見つからない場合,<<-はグローバル環境に変数を新規に作成するが,これは望ましいことではない.

親環境をさかのぼる?親環境って何? グローバル環境に代入するのが望ましくないなら<<-は何に使うの?

まずは環境について説明しましょう。

環境

環境は名前と値を関連付けるものです。

environment()は現在の環境を返します。 ユーザが普段作業をしている環境はグローバル環境です。

environment()
#> <environment: R_GlobalEnv>

環境をリストに変換することで、その中身を見ることができます。

abc <- 123
as.list(environment())
#> $abc
#> [1] 123

現在の環境にある変数abcが見えました。

環境はnew.env()で作成することができます。 しかし実はRユーザは日常的に環境をたくさん作っています。関数の実行環境です。 関数を実行すると、実行するたびに新しい環境が生成されます。これを関数の実行環境と呼びます。

foo <- function() {
  environment()
}
foo()
#> <environment: 0x55949180ab60>
foo()
#> <environment: 0x559491958088>

foo()を実行するたびに新しい環境が生成されています。

実行環境の中身を覗いてみましょう。

foo <- function(a) {
  b <- 456
  as.list(environment())
}
foo(123)
#> $b
#> [1] 456
#> 
#> $a
#> [1] 123

仮引数のaと関数内で代入したbが見えました。 (ちなみに環境内のオブジェクトに順番はありません。よってリストに変換した結果の順番に意味は無いです。)

環境には親があります。parent.env()で親環境を知ることができます。

foo <- function() {
  parent.env(environment())
}
foo()
#> <environment: R_GlobalEnv>

関数の実行環境は、その関数が生成された環境を親に持ちます。 関数fooはグローバル環境で生成されたので、その実行環境の親環境はグローバル環境です。

<<-の挙動は、親環境をさかのぼって最初に見つかった変数を修正する、というものでした。 よってグローバル環境で生成された関数内で<<-を使うと親環境であるグローバル環境の変数を修正します。

foo <- function() {
  abc <<- 234
}
foo()
abc
#> [1] 234

ここまでは<<-はグローバル環境に代入する、という説明のとおりです。 そうではない場合もあります。見てみましょう。

クロージャ

関数内で関数を生成してみます。その親環境はどうなるでしょう。

f <- function() {
  print(environment())
  g <- function() {
    print(parent.env(environment()))
  }
  g()
}
f()
#> <environment: 0x559491432f68>
#> <environment: 0x559491432f68>

外側の関数の実行環境が、そこで生成された関数の親環境であることが確認できました。

よって内側の関数で<<-を使うと、親環境である外側の関数の実行環境に代入します。

f <- function() {
  a <- 123
  g <- function() {
    a <<- 456
  }
  g()
  a
}
f()
#> [1] 456

より実用的な例を挙げます。以下はカウンターを生成する関数です。

make_counter <- function() {
  n <- 0
  function() {
    n <<- n + 1
    n
  }
}

make_counter()によって作られた関数count_aは、自身が呼び出された回数を返します。

count_a <- make_counter()
count_a()
#> [1] 1
count_a()
#> [1] 2
count_a()
#> [1] 3

もう一つカウンターを作ってみましょう。

count_b <- make_counter()
count_b()
#> [1] 1
count_b()
#> [1] 2
count_a()
#> [1] 4

count_acount_bはお互い独立して数を数えることができます。 各自が生成された別々の環境を親環境として参照しているためです。 このように、自身が生成された環境を囲い込んでいる関数のことをクロージャと言います。

参考文献

R言語徹底解説

今回扱った環境を始め、Rの仕様の複雑な部分をHadley Wickhamがものすごく明快に解説しています。 クロージャの実用例も紹介されています。 一通り読めば、よりRの仕様に確信を持ってコードを書けるようになるはずです。 長期休暇に読むと良いかも。Enjoy!