🧰

Rで柔軟な関数作成を実現する環境と表現式についてのまとめ

2023/02/17に公開

はじめに

今回はRの中でも理解に時間がかかる 環境と表現式 についてまとめます。

聞き慣れず、利用している方は少ないと思いますが、知っていると関数内挙動の理解が深まります。また、関数を自ら定義する際にも非常に役に立つツールです。

体系的に紹介しているサイトは少ないので、自分のためにもメモとして残しておきます。

環境について

環境(environment)についてですが、環境とは、コンピュータやRのソフトウェアバージョンのことではなく、R動作時のオブジェクトを保持する領域のことです。

難しいですが、通常、コンソールで作業しているときと、関数やパッケージを利用しているときの作業環境は異なります。

x <- 5
y <- 100
f <- function(x) {
    y <- 1
    x + y
}
x
[1] 5
y
[1] 100
f(x)
[1] 6

環境の構造

上記は簡単な例ですが、関数内で定義した y は関数内でしか呼び出しができません。経験的に理解している人も多いと思いますが、これを形成しているのが 環境(environmental) です。

また、環境の実態は list 構造です。言い換えると、 私たちはlist構造の中で操作を行っており、作成したオブジェクトはこのlistに含まれる要素の一つです。listの中に新たにlist構造を作成すると、これは新たな環境を設定したことと同じことです。

## global environment
env <- list(
    x <- 1,
    y <- 10,

    ## 新たな環境
    env1 <- list(
        x <- 100,
        y <- 500
    ),

    ## 別の環境
    env2 <- list(
        x = 3.1415,
        y = 2.22
    )
)

このような構造だと思ってもらえれば十分です。

また、見て分かるように環境は階層構造を持っています。新たに環境を作る場合は、 親環境(parent) を設定します。(指定しない場合は、現在の環境が自動で選択されます。)

先程の構造は概念図で、実際に環境を設定するには以下のようにします。

## 環境を作成
e <- new.env(parent = .GlobalEnv)
class(e)
[1] "environment"
print(e)
<environment: 0x5583cc396a40>

環境中のオブジェクト操作

環境内に新たに変数などを作成する場合、方法として2通りあります。

e$x <- 1
assign("x", 1, envir = e)

data.frameを操作するように $ を使用して定義する方法と、 assign() を使用する方法です。

どちらでも同じ結果になりますが、筆者としては、どのような場面でも使いやすい assign() がおすすめです。

assign("x", 1, envir = e)
assign("y", 10, envir = e)
assign("f",
       function(x, y) {
           x + y
       }, envir = e)

## 作成した変数の確認
as.list(e)
$x
[1] 1

$y
[1] 10

$f
function(x, y) {
           x + y
       }

環境内での操作

環境に作成したオブジェクトの利用については、data.frameと同じ様に $ を利用できます。

e$x
[1] 1

または、 with を使用と環境内で操作を行うことができ、オブジェクトの呼び出しに $ を使う必要はありません。

x <- 100
y <- -100
with(e, expr = {
    x
})
[1] 1
with(e, expr = {
    f(x, y)
})
[1] 11

attachdetach を使うのと同じ感覚で使うことができます。

最初に定義した x, y の値は、現在の環境(global environment) で定義した値ですので、withを利用した環境内での計算には使用されません。

例外として、eの中に定義していない変数がある場合は、その親環境から呼び出されますので注意が必要です。

z <- 100
with(e, expr = {
    x + z
})
[1] 101

withとwithinについて

withを使うと環境内で操作ができると前章で紹介しましたが、同じ様なことが within() でもできます。

ただし、 within では第一引数に環境(environment)ではなく list を指定します。また、出力は第一引数に指定したlistの中に新たな要素を追加した構造となります。

## irisデータセットの6列目までを取得して使用
d <- as.list(head(iris))
str(d)
List of 5
 $ Sepal.Length: num [1:6] 5.1 4.9 4.7 4.6 5 5.4
 $ Sepal.Width : num [1:6] 3.5 3 3.2 3.1 3.6 3.9
 $ Petal.Length: num [1:6] 1.4 1.4 1.3 1.5 1.4 1.7
 $ Petal.Width : num [1:6] 0.2 0.2 0.2 0.2 0.2 0.4
 $ Species     : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1
## withでの操作
with(d, expr = {
    sum(Sepal.Width)
})
[1] 20.3
## withinでの操作: 何も結果が出ない
within(d, expr = {
    sum(Sepal.Width)
})
$Sepal.Length
[1] 5.1 4.9 4.7 4.6 5.0 5.4

$Sepal.Width
[1] 3.5 3.0 3.2 3.1 3.6 3.9

$Petal.Length
[1] 1.4 1.4 1.3 1.5 1.4 1.7

$Petal.Width
[1] 0.2 0.2 0.2 0.2 0.2 0.4

$Species
[1] setosa setosa setosa setosa setosa setosa
Levels: setosa versicolor virginica
## withinでの操作: 結果が出るパターン
within(d, expr = {
    s <- sum(Sepal.Width)
})
$Sepal.Length
[1] 5.1 4.9 4.7 4.6 5.0 5.4

$Sepal.Width
[1] 3.5 3.0 3.2 3.1 3.6 3.9

$Petal.Length
[1] 1.4 1.4 1.3 1.5 1.4 1.7

$Petal.Width
[1] 0.2 0.2 0.2 0.2 0.2 0.4

$Species
[1] setosa setosa setosa setosa setosa setosa
Levels: setosa versicolor virginica

$s
[1] 20.3
## 環境内に作成したオブジェクトを使用する場合は、環境をlistに置き換えて使用する
within(as.list(e), expr = {
    s <- f(x, y)
})
$x
[1] 1

$y
[1] 10

$f
function(x, y) {
           x + y
       }
<bytecode: 0x5583ca49cb50>

$s
[1] 11

最後の方法では、list構造の中に新たに s という要素が作られていることがわかります。

目的の結果だけがほしい場合は with() を使用し、それ以外の前提条件(説明変数)などもまとめて出力したい場合は within() を使用すると良いでしょう。

環境の探り方

今後、環境を指定してオブジェクトの呼び出しなども行いますので、そのために必要となる操作をまとめておきます。

グローバル環境

コンソール等の環境です。スクリプトを書いて指定したオブジェクトはグローバル環境に存在します。

.GlobalEnv
<environment: R_GlobalEnv>

現在環境の取得

environment()
<environment: R_GlobalEnv>

親環境の取得

引数には環境クラスのオブジェクトを指定します。指定したオブジェクトの親環境を探ります。

parent.env(e)
<environment: R_GlobalEnv>

相対的な親環境の取得

現在の作業環境の親が何であるかを探ります。

関数内で別の関数を使用するなど、複数の環境を跨いで操作を行っている場合に使用します。

dplyrggplot2 ではこの環境操作が頻繁に登場します。

## 引数では階層数を指定
## 親環境の親環境を探る場合は、n = 2を指定する等
parent.frame(n = 1)
<environment: 0x5583cc1277e8>

これらの環境操作は、次にまとめる 表現式 と密接に関わっています。

表現式

Rではコードを書き、そのコードを実行する前に表現式として評価されていない状態を維持することができます。

これは文字列とは明確に異なっており、表現式は list の形式を取る構造体の一つです。

x <- quote(iris)
x
iris
as.list(x)
[[1]]
iris

通常であれば、変数xに irisのデータを入れると、xはdata.frameの構造を持つオブジェクトになりますが、表現式では iris というオブジェクトがそのまま格納されていることがわかります( list 構造でいう、要素名のような状態)。これを eval() することで評価され、irisのデータが呼び出されます。

eval(x)
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.1         3.5          1.4         0.2  setosa
2          4.9         3.0          1.4         0.2  setosa
3          4.7         3.2          1.3         0.2  setosa
4          4.6         3.1          1.5         0.2  setosa
5          5.0         3.6          1.4         0.2  setosa
6          5.4         3.9          1.7         0.4  setosa

表現式を使うメリットは、大きく以下の2点です。

  • 表現式を評価する環境を選択できる
  • 表現式を評価するタイミングを自由に設定できる
  • 表現式を修正できる

パッケージ作成者以外にはあまり良くわからないかもしれませんが、上2つは関数を作成するときに利用します。

また、表現式を操作するときに筆者がよく使う関数は、主に以下の5つです。

  • quote
  • bquote
  • substitute
  • expression
  • eval

以降に、筆者がよく使う方法をまとめていきます。

表現式の作成

ここでは、主に関数作成時において表現式がどのように見見えるのか、まとめます。

f <- function(x) {
  list(test1 = x,
       test2 = quote(x),
       test3 = substitute(x))
}
f(5)
$test1
[1] 5

$test2
x

$test3
[1] 5
f(head(iris))
$test1
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.1         3.5          1.4         0.2  setosa
2          4.9         3.0          1.4         0.2  setosa
3          4.7         3.2          1.3         0.2  setosa
4          4.6         3.1          1.5         0.2  setosa
5          5.0         3.6          1.4         0.2  setosa
6          5.4         3.9          1.7         0.4  setosa

$test2
x

$test3
head(iris)

違いをまとめると、 quote では変数名として表現式が作成され、 substitute では入力したコードがそのまま表現式になっています。

では、これらの構造が見えるように as.list として見てみます。

f <- function(x) {
  list(test1 = x,
       test2 = as.list(quote(x)),
       test3 = as.list(substitute(x)))
}
f(5)
$test1
[1] 5

$test2
$test2[[1]]
x


$test3
$test3[[1]]
[1] 5
f(head(iris))
$test1
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.1         3.5          1.4         0.2  setosa
2          4.9         3.0          1.4         0.2  setosa
3          4.7         3.2          1.3         0.2  setosa
4          4.6         3.1          1.5         0.2  setosa
5          5.0         3.6          1.4         0.2  setosa
6          5.4         3.9          1.7         0.4  setosa

$test2
$test2[[1]]
x


$test3
$test3[[1]]
head

$test3[[2]]
iris

substitute で表現式にしたtest3では、表現式が関数名とオブジェクト名に分けられています。

これは、本来は quote でも同じことことになりますが、quoteではコードを直接表現式にした場合のみ階層構造を見ることができます。

q <- quote(head(iris))
q
head(iris)
as.list(q)
[[1]]
head

[[2]]
iris

このような理由から、関数内に 関数や式(formula)を表現式として取り込む場合は 引数を substitute を使って取り込む必要があります。

それ以外の場合(変数等)は単に quote とすれば問題ありません。

f <- function(x, y) {
    x <- quote(x)
    y <- substitute(y)
    ...
}

評価(eval)の使い方

それでは、取り込んだ表現式を使って処理を行ってみます。

f <- function(x, y) {
    eval(quote(x + y))
}
f(1, 5)
[1] 6

当たり前ですが、合計値である6が出力されます。

今度は実行環境を指定して評価してみます。

x <- 100
y <- 100
f <- function(x, y) {
    e <- parent.frame()
    eval(quote(x + y), envir = e)
}
f(1, 5)
[1] 200

更に、xだけ引数指定した変数値を使用するように修正してみます。

このような場合はquote の変わりに bquote を使います。

bquoteでは .() で囲った部分だけは評価され、それ以外はそのまままとめて表現式となります。

x <- 100
y <- 100
f <- function(x, y) {
    e <- parent.frame()
    eval(bquote(.(x) + y), envir = e)
}
f(1, 5)
[1] 101
## 参考
bquote(.(x) + y)
100 + y

また bquote でも where で以下のように評価する環境を選択できます。

x <- 100
y <- 100
f <- function(x, y) {
    e <- parent.frame()
    eval(bquote(.(x) + y, where = e), envir = e)
}
f(1, 5)
[1] 200

substituteの使い方

それでは次に、引数に式(formula)を指定する場合です。上に記載したとおり、このときは substitute を使うことが便利です。

f <- function(x, arg) {
    force(x)  
    arg <- substitute(arg)
    eval(arg, envir = x)
}
f(x = iris, arg = {Species == "setosa"})
  [1]  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE
 [13]  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE
 [25]  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE
 [37]  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE
 [49]  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
 [61] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
 [73] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
 [85] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
 [97] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[109] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[121] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[133] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[145] FALSE FALSE FALSE FALSE FALSE FALSE

何が起きたかと言うと、以下の式が実行されたときと同じ挙動です。

iris$Species == "setosa"
with(iris, expr = {Species == "setosa"})

with は先に説明したとおり、環境内で操作を行う場合に用いる関数でした。

eval でも、引数の環境にlistやdata.frameを指定すると、要素名を変数名として使用することができます。これは、Rの環境(environment)構造が list と同じ構造だということに起因しています。

次に、data.frameから該当するデータを全て抜き出すように修正します。

f <- function(x, arg) {
    force(x)  
    arg <- substitute(arg)
    pos <- eval(arg, envir = x)
    x[pos, ]
}
f(x = iris, arg = {Species == "setosa"})
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1           5.1         3.5          1.4         0.2  setosa
2           4.9         3.0          1.4         0.2  setosa
3           4.7         3.2          1.3         0.2  setosa
4           4.6         3.1          1.5         0.2  setosa
5           5.0         3.6          1.4         0.2  setosa
6           5.4         3.9          1.7         0.4  setosa

subset()dplyr::filter() と同じ結果が現れました。

では、途中で表現式を修正してみます。

f <- function(x, arg) {
    force(x)  
    arg <- substitute(arg)
    arg[[2]][[1]] <- substitute(`!=`)
    pos <- eval(arg, envir = x)
    x[pos, ]
}
f(x = iris, arg = {Species == "setosa"})
    Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
51           7.0         3.2          4.7         1.4 versicolor
52           6.4         3.2          4.5         1.5 versicolor
53           6.9         3.1          4.9         1.5 versicolor
54           5.5         2.3          4.0         1.3 versicolor
55           6.5         2.8          4.6         1.5 versicolor
56           5.7         2.8          4.5         1.3 versicolor
57           6.3         3.3          4.7         1.6 versicolor
58           4.9         2.4          3.3         1.0 versicolor
59           6.6         2.9          4.6         1.3 versicolor
60           5.2         2.7          3.9         1.4 versicolor
61           5.0         2.0          3.5         1.0 versicolor
62           5.9         3.0          4.2         1.5 versicolor
63           6.0         2.2          4.0         1.0 versicolor
64           6.1         2.9          4.7         1.4 versicolor

表現式を分解してみると以下のようになります。

a <- substitute({Species == "setosa"})
as.list(a)
[[1]]
`{`

[[2]]
Species == "setosa"
as.list(a[[2]])
[[1]]
`==`

[[2]]
Species

[[3]]
[1] "setosa"

ここで、比較演算子である a[[2]][[1]]==!= に書き換えたのが上の操作です。

そうすると、もともと入力した Species == "setosa" という条件式が Species != "setosa" となって評価さるようになりました。

表現式で自由な計算を実行

dplyr パッケージの summarise_at 関数では、引数に 要素(列名)と関数名 を設定します。

最後に、これと同じような挙動をする関数を作成してみます。

f <- function(x, vars = c(), funs = list()) {
  force(x)
  vars <- substitute(vars)
  funs <- substitute(funs)

  ## データ列の取得
  vars[[1]] <- substitute(data.frame)
  x <- eval(vars, envir = x)

  ## 関数名を列名用で取得
  name <- do.call(c, Map(deparse, funs[-1]))

  ## 関数を適用
  d <- do.call(rbind,
          lapply(eval(funs), function(f) {
            do.call(cbind, Map(f, x))
          }))

  ## 行名を関数名に変更
  row.names(d) <- name
  d
}
f(x = iris, vars = c(Sepal.Width, Sepal.Length), funs = list(sum, mean))
     Sepal.Width Sepal.Length
sum   458.600000   876.500000
mean    3.057333     5.843333

使える関数は限られていますが、概ね同じような挙動をする関数を作成することができました。

なお、一度しか登場しない内部処理をする場合は local() を使って操作すると、全く新しい環境内で目的の操作を実行することができるため、この方法もおすすめです。

終わりに

Rでメタプログラミングを行う際に用いる技法として、環境と表現式というものについて使い方をまとめましたが、これらはあくまでツール(道具や材料)です。

ツールをうまく使うことにより、完成物や、より便利な道具を出に入れることができます。あまり深く考えずにどんどん試してみて理解を深めていってください。

Discussion