Rのこと。

記事は引っ越し作業中。2023年中までに引っ越しを完了させてブログは削除予定

Advanced R: Environments

はじめに

Advanced R 2nd EditionのPaperBook版が届いたので、Rについてのおさらい備忘録。また、書籍を読んでいるにも関わらず誤った解釈や用語の誤用もあるかもしれませんので、参考にする際は自己責任でお願いします。

この記事のライセンスはAdvanced R 2nd Editionと同様で下記の通りです。

Environments

ここでは環境の構造、スコープとの関係について説明されている。

環境の基本

環境は名前付きリストに似ているが、4つの重要な違いある。

  • すべての名前は一意でなければならない。
  • 環境内の名前は順序付けられない。
  • 環境は親を持つ。
  • 変更時に環境はコピーされない。

Advanced R 2nd Editionの図を借りて説明すると、環境とは、名前のセットを値のセットにバインディングするためのもの。env_print()で環境内の情報を確認できる。また、env_names()では、環境がバインディングしている名前が確認できる。 f:id:AZUMINO:20190730204751p:plain

library(rlang)
e1 <- env(
  a = FALSE,
  b = "a",
  c = 2.3,
  d = 1:3,
)

env_print(e1)

<environment: 0x7ffbb16ac978>
  parent: <environment: global>
  bindings:
  * a: <lgl>
  * b: <chr>
  * c: <dbl>
  * d: <int>

env_names(e1)
[1] "a" "b" "c" "d"

普段作業しているワークスペースグローバル環境であり、それを確認するためにはcurrent_env()が使える。この関数は現在コードが実行されている環境を表示する。

current_env()
<environment: R_GlobalEnv>

identical(global_env(), current_env())
[1] TRUE

環境のルールとして親(parents)を持つ。またまたAdvanced R 2nd Editionの図を借りると、環境のイメージとして、青い円が環境であり、親との関係は青い円と矢印として表示される。親はレキシカルスコープを機能するために使用される。名前が環境内に見つからない場合、Rはその親を調べ、そこにないのであれば、さらに親環境を調べる。これがレキシカルスコープ。 f:id:AZUMINO:20190730205427p:plain

e2bの親環境は0x7ffbb07889b0であり、e2aの親環境はR_GlobalEnvとなる。

e2a <- env(d = 4, e = 5)
e2b <- env(e2a, a = 1, b = 2, c = 3)

env_parent(e2b)
<environment: 0x7ffbb07889b0>

env_parent(e2a)
<environment: R_GlobalEnv>

レキシカルスコープにも終りがある。それが例外的に親環境を持たない空環境のこと。この環境まで遡ると、レキシカルスコープは探索を終了する。

e2c <- env(empty_env(), d = 4, e = 5)

env_parents(e2c)
[[1]] $ <env: empty>

env_parents(empty_env())
list()

スーパーアサインメント<<-

通常の代入<-は、常に現在の環境で名前が値をバインディングする。スーパーアサインメント<<-は、現在の環境で変数を作成するのではなく、親環境で見つけた既存の変数を変更する。見つからない場合は、その変数を作ります。

x <- 0
f <- function() {
  x <<- 1
}

f()
x
[1] 1

環境の再帰

環境のすべての親を確認したい場合、再帰関数を書くと便利とのこと。where()は、Rの通常のスコープ規則を使用して、その名前が定義されている環境を表示する。

where <- function(name, env = caller_env()) {
  if (identical(env, empty_env())) {
    # Base case
    stop("Can't find ", name, call. = FALSE)
  } else if (env_has(env, name)) {
    # Success case
    env
  } else {
    # Recursive case
    where(name, env_parent(env))
  }
}

where()は、空環境に到達しするとエラーを返し、名前を見つけた場合、その環境を返す。また、その環境で名前が見つからないと親環境を探しに行く。

where("yyy")
エラー: Can't find yyy 

x <- 5
where("x")
<environment: R_GlobalEnv>

where("mean")
<environment: base>

サーチパス

library()require()でパッケージを読み込むと、グローバル環境の親の1つとなる。グローバル環境の直接の親環境は最後にロードしたパッケージ。この例では、{c}がグローバル環境の親環境となる。 f:id:AZUMINO:20190730213306p:plain {d}を読み込むとグローバル環境の親環境は{c}ではなく{d}となる。 f:id:AZUMINO:20190730213626p:plain サーチパスを確認したければ、search_envs()で確認できる。

search_envs()
 [[1]] $ <env: global>
 [[2]] $ <env: package:rlang>
 [[3]] $ <env: tools:rstudio>
 [[4]] $ <env: package:stats>
 [[5]] $ <env: package:graphics>
 [[6]] $ <env: package:grDevices>
 [[7]] $ <env: package:utils>
 [[8]] $ <env: package:datasets>
 [[9]] $ <env: package:forcats>
[[10]] $ <env: package:stringr>
[[11]] $ <env: package:dplyr>
[[12]] $ <env: package:purrr>
[[13]] $ <env: package:readr>
[[14]] $ <env: package:tidyr>
[[15]] $ <env: package:tibble>
[[16]] $ <env: package:ggplot2>
[[17]] $ <env: package:tidyverse>
[[18]] $ <env: package:methods>
[[19]] $ <env: Autoloads>
[[20]] $ <env: package:base>

関数と環境

関数は、作成時に現在の環境をバインドする。これは関数環境(function environment)と呼ばれ、レキシカルスコープのために使われる。環境を捉える(囲む)関数はクロージャと呼ばれる。fn_env()は関数環境を表示する。

y <- 1
f <- function(x) x + y
fn_env(f)
<environment: R_GlobalEnv>

またまたAdvanced R 2nd Editionの図を借りる。関数のイメージは、環境を結び付ける丸みのある長方形である。この場合、名前fが関数f()にバインドし、関数が環境をバインディングしている。 f:id:AZUMINO:20190730214213p:plain しかし、必ずこうなるとは言えない。次の例では、名前gが関数をバインディングし、関数g()は、グローバル環境をバインディングしている。 f:id:AZUMINO:20190730214610p:plain

名前空間(Name Space)

名前空間の目的は、どのパッケージがロードされているかにかかわらず、すべてのパッケージが同じように機能すること。パッケージ内のすべての関数は、パッケージ環境とネームスペース環境という2つの環境に関連付けられる。

  • パッケージ環境には、どこからでもアクセス可能な関数が含まれており、サーチパスに位置する。
  • 名前環境には、インターナル関数を含め、すべての関数が含まれるが、その親環境には、パッケージが必要とするすべての関数への束縛を含む特別なインポート環境がある。

コンソールでvarとタイプするとグローバル環境で見つかるが、sd()からすると、var()名前空間環境で見つけるので、グローバル環境は探索されない。 f:id:AZUMINO:20190730215545p:plain

var <- function(x, na.rm = TRUE) 100
var
function(x, na.rm = TRUE) 100

x <- 1:10
sd(x)
[1] 3.02765

基本的に、名前空間の親環境はグローバル環境。つまり、バインディングがインポート環境で定義されていない場合、パッケージは通常の方法で名前を探す。具体的に、インポート環境は、NAMESPACEファイルを使用してパッケージ開発者によって制御される。

実行環境

下記を実行すると、フレッシュスタートによって、毎回同じ値が返される。つまり、関数が呼び出されるたびに、実行をホストするための新しい環境が作成される。これを実行環境と呼び、その親を関数環境と呼ぶ。

g <- function(x) {
  if (!env_has(current_env(), "a")) {
    message("Defining a")
    a <- 1
  } else {
    a <- a + 1
  }
  a
}

# フレッシュスタート
g(10)
Defining a
[1] 1

# フレッシュスタート
g(10)
Defining a
[1] 1

実行環境と関数環境の関係は下記のイメージ図のとおり。関数から見て、上の箱が実行環境で右の箱が関数環境。

h <- function(x) {
  # 1.
  a <- 2 # 2.
  x + a
}
y <- h(1) # 3.

f:id:AZUMINO:20190730221119p:plain

コールスタック

f()g()を呼び、g()h()を呼び出すとき、呼び出し手順はどうなるのか。

f <- function(x) {
  g(x = 2)
}
g <- function(x) {
  h(x = 3)
}
h <- function(x) {
  stop()
}

traceback()でも調べることができるが、lobstr::cst()を使うと便利。cst()h()から呼び出され、h()g()から呼び出され、g()f()から呼びだれていることがわかる。

h <- function(x) {
  lobstr::cst()
}

f(x = 1)
    █
 1. └─global::f(x = 1)
 2.   └─global::g(x = 2)
 3.     └─global::h(x = 3)
 4.       └─lobstr::cst()

フレーム(Frame)

呼び出しスタックの各要素は、評価コンテキストとも呼ばれるフレーム。フレームには3つの重要な要素がある。

  • expr関数で呼び出しを与える式。
  • envでラベリングされる環境。通常は関数の実行環境。例外が2つあり、グローバルフレームの環境はグローバル環境であり、eval()を呼び出すことは、フレームを生成すること。
  • 親、コールスタック内の前のコール(灰色の矢印で表示)。

下記はf(x = 1)を呼び出し際のコールスタックを示す。 f:id:AZUMINO:20190730221837p:plain