記事の概要
この記事は、R で普段あまり意識されない「環境」environment
と「式(あるいは表現式)」expression
について、その基本的な操作や特徴を整理するものです。
環境と式を一つの記事にまとめる理由は、「関数の中から呼び出し環境(親フレーム)の変数に変更を加える」とか「関数の中でデータフレームを環境に見立てて変数を加工したデータフレームを返す」といった機能を持つ関数を自作する場合などに、環境と式の両方に関する知識が必要になるという点にあります。
なお、記事は著者自身による参照のために作成されており、コードの説明などは著者自身の備忘に資するように書かれています。また、R にはモデル式を表す formula
というオブジェクトも存在しますが、この記事はモデル式には言及しません。
環境 environment
環境の基本操作
## 環境(environment)を生成する
E <- new.env()
E <- rlang::env(hoge = 1, fuga = 2, piyo = 3)
## 環境内の変数に値を代入する
E[["hoge"]] <- 1 # `[[]]<-`関数
E$fuga <- 2 # `$<-`;関数
assign("piyo", 3, envir = E) # assign()関数
eval(quote(nyaa <- 4), envir = E) # eval()関数
## 環境内の変数の値を参照する
E[["fuga"]]
#> [1] 2
E$piyo
#> [1] 3
eval(quote(nyaa), envir = E)
#> [1] 4
get0("hoge", envir = E)
#> [1] 1
## 一部の方法は環境のサーチパスから変数の値を探すことに注意
foo <- 5 # グローバル環境に定義
E[["ghoge"]] # 存在しない
#> NULL
eval(quote(foo), envir = E)
#> [1] 5
get0("foo", envir = E)
#> [1] 5
## 環境の中で式を評価する
eval(quote(hoge + fuga == piyo), envir = E)
#> [1] TRUE
## 環境内の変数の一覧を取得する
ls(E, sorted = FALSE) # 辞書順ソートをしない方がわずかに高速
#> [1] "hoge" "piyo" "nyaa" "fuga"
objects(E) # alias
#> [1] "fuga" "hoge" "nyaa" "piyo"
names(E) # 高速だが定義変数以外のオブジェクトが含まれる可能性がある
#> [1] "hoge" "piyo" "nyaa" "fuga"
## 環境内の変数を削除する
rm(fuga, envir = E)
remove(list = c("hoge", "nyaa"), envir = E) # alias
E$piyo <- NULL # リストと違って削除にならない
ls(E)
#> [1] piyo
親環境と越境代入 <<-
new.env()
関数の引数 parent
を通じて親環境を指定できる。越境代入演算子 <<-
は、代入先変数名が自身を除くサーチパスの中に存在する場合はその変数の値を書き換えるが、存在しない場合はグローバル環境に新たに変数を作成する。関数内で用いる場合は親環境と親フレーム(呼出環境)の違いにも注意する。
## 親環境を指定して環境を生成する
E2 <- new.env(parent = baseenv()) # 親環境を base に指定
E3 <- new.env(parent = emptyenv()) # 親環境なし(空環境)
## 環境ごとのサーチパスにない関数は使えない
exists("+", envir = E2)
#> [1] TRUE
exists("+", envir = E3) # 基本的な演算子も存在しない
#> [1] FALSE
eval(quote(1+1), envir = E2)
#> [1] 2
eval(quote(1+1), envir = E3)
#> Error in 1 + 1 : could not find function "+"
exists("rnorm", envir = E2) # base にない関数は見つからない
#> [1] FALSE
## 越境代入(super assignment)
E4 <- new.env(parent = E2) # <<- は base の演算子
E2$hoge <- 0
eval(quote(hoge <<- 1), envir = E4) # E3 の hoge を書き換える
E2$hoge
#> [1] 1
eval(quote(fuga <<- 2), envir = E4) # .GlobalEnv の fuga を書き換える
E2$fuga
#> NULL
fuga
#> [1] 2
## 関数内部での越境代入
f <- function() hoge <<- 1
supassign <- function() {
hoge <- 0
f() # 関数 f の実行環境の親環境は .GlobalEnv
cat(hoge, "\n")
g <- function() hoge <<- 2
g()
cat(hoge, "\n")
h <- function()
eval(quote(hoge <- 3), envir = parent.frame())
h()
cat(hoge, "\n")
}
supassign()
#> 0
#> 2
#> 3
hoge
#> [1] 1
Copy-on-Modify の例外
環境と他のオブジェクトの大きな違いの一つは Copy-on-Modify(変更を加えるまでは参照を渡し、変更を加えるときにコピーを作る)というRオブジェクトの原則に則らないという点にある。たとえばリストなどの場合、参照を渡した先の変数に変更を加えるとその時点でリストがコピーされ、コピーに対して変更が反映される。一方、環境の場合は、参照を渡した先の変数に加えた変更も元の環境に直接反映される。
## リストと環境を生成する
L <- list(hoge = 1, fuga = 2, piyo = 3)
E <- as.environment(L)
## 参照を渡した変数に変更を加える
L2 <- L
E2 <- E
L2$hoge <- 2 # リストは変更時にコピーされる
L$hoge
#> 1
E2$hoge <- 2 # 環境はインプレースに変更される
E$hoge
#> 2
## 関数に渡した変数に関数の中で変更を加える
f <- function(object)
object$hoge <- object$fuga * object$piyo
f(L) # 変更時に関数の実行環境内にコピーされる
L$hoge
#> 1
f(E) # 環境はインプレースに変更される
E$hoge
#> 6
関数の実行環境と呼び出し環境
実行環境(式が評価される環境)を知りたいときは、rlang::current_frame()
関数を用いる。また、関数自体の実行環境ではなく関数を呼び出した環境(親フレーム)を知りたいときは、parent.frame()
や rlang::caller_env()
関数を用いる。
## 現在の実行環境(フレーム)を調べる
rlang::current_env()
#> <environment: R_GlobalEnv>
(function() {rlang::current_env()})()
#> <environment: 0x...hoge(生成される関数内環境)>
## 再帰呼び出し関数の親フレームを遡る
f <- function(maxit = 10L) {
n <- 1L
print.default(rlang::current_env())
for (i in seq_len(maxit)) {
n <- n + 1L
frm <- parent.frame(n = n)
print.default(frm)
if (identical(frm, .GlobalEnv))
break
}
}
g <- function(iter = 10L, maxit = 10L) {
if (iter > 1L)
g(iter = iter - 1L, maxit = maxit)
else
f(maxit = maxit)
}
g(3L)
#> <environment: 0x...hoge>
#> <environment: 0x...fuga>
#> <environment: 0x...piyo>
#> <environment: R_GlobalEnv>
関数の実行環境ではない別の環境、たとえばその関数を呼び出した親フレームなどに定義されているオブジェクトを直接変更したいときは、eval()
関数の引数 envir
で指定するフレームで式を評価することができる。
## リストの末尾に値を追加する関数(高速ではないが計算量のオーダーはまとも)
lappend <- function(x, value) {
x <- substitute(x)
stopifnot(is.list(eval(x, envir = parent.frame())))
cl <- call("<-", call("[[", x, call("+", call("length", x), 1L)), value)
eval(cl, envir = parent.frame())
invisible(NULL)
}
L <- list()
lappend(L, "hoge")
lappend(L, "fuga")
L
#> [[1]]
#> [1] "hoge"
#> [[2]]
#> [1] "fuga"
ハッシュテーブルとしての活用
環境は R におけるハッシュテーブルとして利用できる。assign()
関数や get0()
関数を Vectorize()
関数でベクトル化した関数を定義しておくと多数の値を一括で代入、取得する際に役立つ。
## 環境の生成
H <- new.env(parent = enptyenv())
## 値の代入
H$a <- 1:10
H$b <- c("hoge", "fuga", "piyo")
H$c <- NULL
vassign <- Vectorize(assign, c("x", "value")) # 一括代入用の関数
vassign(c("hoge", "fuga", "piyo"), 1:3, envir = H)
## 値の取得
H$b
#> [1] "hoge" "fuga" "piyo"
H$d # 未定義のキー
#> NULL
vget0 <- Vectorize(get0, "x") # 一括取得用の関数
vget0(c("hoge", "fuga", "piyo"), envir = H)
#> hoge fuga piyo
#> 1 2 3
## 値の存在判定
exists("c", envir = H) # 親環境を空にした場合のみ可能
#> [1] TRUE
exists("d", envir = H) # 親環境を空にした場合のみ可能
#> [1] FALSE
!is.null(H$a) # NULLを基準にする場合
#> [1] TRUE
!is.null(H$c) # NULLを基準にする場合
#> [1] FALSE
## 値の削除
rm("b", envir = H)
H$b <- NULL # NULLを基準にする場合
## キーの取得
names(H)
#> [1] "a" "c"
ls(H, sorted = FALSE)
#> [1] "a" "c"
names(H)[!vapply(H, is.null, TRUE)] # NULLを基準にする場合
#> [1] "a"
式 expression
式に関する基本事項
Hadley Wickham 流の定義によれば、式 "expressions" とはコードを解析することで得られる Abstract Syntax Trees(抽象構文木、AST)として表現されうるデータ構造であり、その種類として「定数」"constant"、「記号」"symbol"(「名前」"name" も同じ意味)、「関数呼び出し」"call" の3つが重要である。なお、lobstr::ast()
関数で AST を描くことができる。
## 定数(NULL または長さ 1 のベクトル)
lobstr::ast(1L)
#> 1L
identical(quote(TRUE), TRUE) # 定数は式としても自信に等しい
#> [1] TRUE
quote(2) * quote(3)
#> [1] 6
## 記号(変数名)
lobstr::ast(hoge)
#> hoge
is.symbol(quote(hoge))
#> [1] TRUE
is.name(quote(hoge))
#> [1] TRUE
as.character(quote(hoge)) # 文字列に変換できる
#> [1] "hoge"
## 関数呼び出し
lobstr::ast(hoge <- fuga + 1)
#> █─`<-`
#> ├─hoge
#> └─█─`+`
#> ├─fuga
#> └─1
lobstr::ast(`<-`(hoge, `+`(fuga,1))) # 接頭辞表記でも AST は同じ
#> █─`<-`
#> ├─hoge
#> └─█─`+`
#> ├─fuga
#> └─1
e <- quote(`<-`(hoge, `+`(fuga,1))) # コンソール出力は中置式表記
e
#> hoge <- fuga + 1
length(e)
#> [1] 3
e[[1]] # 第 1 要素は関数庫(function position)であり関数が格納される
#> `<-`
as.list(e[-1]) # 第 2 要素以降に引数を表す式が格納される
#> [[1]]
#> hoge
#> [[2]]
#> fuga + 1
式の基本操作
## 式(expression)を生成する
e1 <- quote(hoge <- 1)
e1
#> hoge <- 1
e2 <- rlang::expr(fuga <- 2)
e2
#> fuga <- 2
E <- rlang::env(hoge = 1)
substitute(hoge + 1)
#> hoge + 1
substitute(hoge + 1, env = E) # substitute() は指定環境で評価した式を返す
#> 1 + 1
## 文字列を式に変換する
e3 <- str2lang("piyo <- 3")
e3
#> piyo <- 3
e4 <- rlang::parse_expr("nyaa <- 4")
e4
#> nyaa <- 4
## 式を評価する
eval(e1) # グローバル環境に hoge が定義される
hoge
#> [1] 1
E <- new.env()
eval(e2, envir = E) # 環境 E に fuga が定義される
E$fuga
#> [1] 2
eval(quote(hoge + fuga), envir = E)
#> [1] 3
## 式を文字列に変換する
deparse(e1)
#> [1] "hoge <- 1" # 長い式に対して長さ 2 以上のベクトルを返す
rlang::expr_text(e2)
#> [1] "fuga <- 2"
遅延評価
関数の引数の規定値は式であり、関数の内部で参照される時にはじめて評価される。
f <- function(x = x + 1) x # x は未定義でも OK
x <- 1L
f(x + 1) # 実行時に引数に渡すときはその時点で評価
#> [1] 2
f() # 遅延評価の時点で循環する
#> Error in f() :
promise already under evaluation: recursive default argument reference or earlier problems?
関数を呼び出した式の取得 match.call()
関数内で match.call()
関数を用いると、その関数を呼び出した式(関数呼び出し "call")を取得できる。
f <- function(...) match.call()
cl <- f(hoge = TRUE, fuga = hoge, piyo = )
cl[[1]]
#> f
as.list(cl[-1])
#> $hoge
#> [1] TRUE
#> $fuga
#> hoge
#> $piyo
#>
deparse(cl)
[1] "f(hoge = TRUE, fuga = hoge, piyo = )"
この関数を応用することで、引数に与えられた式を、関数内で用意した別の環境の中で評価して用いることができる。以下の例において、関数 g()
は第 1 引数に与えられたデータフレームを環境に見立て、...
に渡された式をその環境で評価することによって得られる列を持つデータフレームを出力する。
# データフレームを加工する関数
g <- function(x, ...) {
cl <- match.call()
exprs <- as.list(cl[-c(1:2)])
res <- list()
for (expr in exprs) {
res[[deparse(expr)]] <- eval(expr, envir = x)
}
names(res) <- names(exprs)
as.data.frame(res)
}
X <- data.frame(hoge = 1:3, fuga = 4:6,
piyo = c("Alice", "Bob", "Charlie"))
X
#> hoge fuga piyo
#> 1 1 4 Alice
#> 2 2 5 Bob
#> 3 3 6 Charlie
g(X, dihoge = hoge * 2, hoga = paste0(hoge, fuga),
pi = substr(piyo, 1, 2))
#> dihoge hoga pi
#> 1 2 14 Al
#> 2 4 25 Bo
#> 3 6 36 Ch
式ベクトル
expression()
関数や parse()
関数の出力は式を要素とするリストであり、Hadley Wickham 流の定義では「式ベクトル」"expression vector" と呼ばれる。R のドキュメントに現れる "expression" という表現はやや曖昧で、式を指すことも式ベクトルを指すこともある。
# 式を要素とするリスト(式ベクトル)を返す関数
exp <- expression(hoge, fuga + 1)
exp[[1]]
#> hoge
exp[[2]]
#> fuga + 1
exp <- parse(text = "hoge; fuga + 1")
exp[[1]]
#> hoge
exp[[2]]
#> fuga + 1
exp <- str2expression("hoge; fuga + 1") # 結果は同じ
exp <- rlang::parse_exprs("hoge; fuga + 1") # リストとして返す
参考資料
環境
式