Rのこと。

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

Rの構文解析

はじめに

この記事はAdvanced R by Hadley Wickhamの内容を自分の備忘録としてまとめたものです。Rのことを知りたいなら、このブログを読む前にAdvanced R by Hadley Wickhamを読むこと絶対的におすすめします。学び多い。

Expressions

表現式を使って計算するというのはどういうことでしょうか。その点の理解を深めていきたいと思います。例えば、y <- x * 10を実行するとxがない、と怒られるわけですね。

y <- x * 10
 エラー:  オブジェクト 'x' がありません

rlang::expr()を使うことで、評価はせずに表現式だけ補足することが可能なので、エラーは返ってきません。

z <- rlang::expr(y <- x * 10)
z

y <- x * 10

そして、この表現式を評価して実行したければeval()xを用意すれば問題なく実行できます。

x <- 4
eval(z)
y
[1] 40

この一連の流れが、コードから表現式を補足して評価するというものです。

Expressions

constant scalars

表現式は、スカラー定数、シンボル、コール・オブジェクト、およびペアリストを持ちます。

まず、スカラー定数は、ASTの最も単純な構成要素。NULLか、長さ1のアトミックベクターなんかが定数です。アトミックベクターtypeof()characterintegerdoblelogicalcomplexrawです。factorはデータ型がintegerの属性がfactorクラスということなので、factorというアトミックベクターはありません。

identical(expr(TRUE), TRUE)
[1] TRUE

identical(expr(1), 1)
[1] TRUE

identical(expr(2L), 2L)
[1] TRUE

identical(expr("x"), "x")
[1] TRUE

symbols

次はシンボル。シンボルは、xmtcarsrnormのようなオブジェクトの名前のことです。is.symbol()で判断できます。シンボルを作成するには、2つの方法があり、expr()でオブジェクトを参照するコードを捕捉する方法と、rlang::sym()で文字列をシンボルに変換する方法です。

expr(rnorm(10))は呼び出しオブジェクトでシンボルオブジェクトでありませんが、expr(rnorm)はシンボルオブジェクトです。

is.symbol(expr(rnorm(10)))
[1] FALSE

is.call(expr(rnorm(10)))
[1] TRUE

is.symbol(expr(rnorm))
[1] TRUE

is.symbol(sym("rnorm"))
[1] TRUE

call objects

コールオブジェクトは呼び出しオブジェクトとか呼ばれるものです。先程の例にもあるようにexpr(rnorm(10))は呼び出しオブジェクトです。

 lobstr::ast(rnorm(n = 10, mean = 10, sd = 1))
█─rnorm 
├─n = 10 
├─mean = 10 
└─sd = 1 

x <- expr(rnorm(n = 10, mean = 10, sd = 1))
typeof(x)
[1] "language"

is.call(x)
[1] TRUE

呼び出しオブジェクトはリストのように振る舞うので、リストのように[[で操作が可能です。

as.list(x)
[[1]]
rnorm

$n
[1] 10

$mean
[1] 10

$sd
[1] 1

call_standardise()は、引数のマッチングに関する柔軟な規則を回避するために使われます。

call_standardise(x)
rnorm(n = 10, mean = 10, sd = 1)

Parsing and grammar

Rの構文解析を行います。正直、普通に使う文なら意識することはあまりありませんが、抽象構文ツリーを使って構文解析してみます。

Rは演算子の優先順位というルールがあります。捉えようによったら、(1 + 2) * 3)(1 + (2 * 3)とも捉えられるかもしれませんが、Rでは、この抽象構文ツリーのとおり、2*3が計算され、それに+1が計算されます。

lobstr::ast(1 + 2 * 3)
█─`+` 
├─1 
└─█─`*` 
  ├─2 
  └─3 

こんな算術演算であれば小学校でも習うので、あれですが、!x %in% yはどうでしょうか。これも抽象構文ツリーを使えばわかります。「xの中でyと一致するものを判断してから否定する」という流れのようです。

lobstr::ast(!x %in% y)
█─`!` 
└─█─`%in%` 
  ├─x 
  └─y 

こんな感じ。x %in% yを計算して!するということですね。

x <- 1:3
y <- c(2:3,5)

x %in% y
[1] FALSE  TRUE  TRUE

!(x %in% y)
[1]  TRUE FALSE FALSE

値を足すのみという場合においても順序が存在します。Rでは、ほとんどの演算子は左結合です。すなわち、左の演算が最初に評価されていきます。

lobstr::ast(1+2+3+4+5)
█─`+` 
├─█─`+` 
│ ├─█─`+` 
│ │ ├─█─`+` 
│ │ │ ├─1 
│ │ │ └─2 
│ │ └─3 
│ └─4 
└─5 

Infix calls

中置呼び出し(Infix calls)というのも確認しておきます。

y <- x * 10

`<-`(y, `*`(x, 10))

lobstr::ast(y <- x * 10)
█─`<-` 
├─y 
└─█─`*` 
  ├─x 
  └─10 

lobstr::ast(`<-`(y, `*`(x, 10)))
█─`<-` 
├─y 
└─█─`*` 
  ├─x 
  └─10 

expr(y <- x * 10)
y <- x * 10

expr(`<-`(y, `*`(x, 10)))
y <- x * 10

Rのコード

はじめに

この記事はAdvanced R by Hadley Wickhamの内容を自分の備忘録としてまとめたものです。Rのことを知りたいなら、このブログを読む前にAdvanced R by Hadley Wickhamを読むこと絶対的におすすめします。学び多い。

Code is data

Rのコードという抽象的なのですが、Rのコードそのものについて考えたことありますか…。僕はありません。なので、Rのコードそのものについての理解を深めたい。それがこのブログ記事の動機です。

library(rlang)
library(lobstr)

「コードはデータ」なので捕捉することができます。日本語の解説なんかでは「Rの表現式を捕捉する」なんて表現されています。昔なんかはquote()substitute()なんかで表現式を捕捉してましたが、ナウイ方法はexpr()でコードを捕捉できます。表現式なので返り値に[1]がありません。

expr(mean(x, na.rm = TRUE))
mean(x, na.rm = TRUE)

expr(10 + 100 + 1000)
10 + 100 + 1000

関数に入力された表現式を補足するには、expr()でダメでenexpr()を使います。quote()substitute()の流れと同じですね。enexpr()の"en"は"enrich"に由来するようです。

capture_it <- function(x) {
  expr(x)
}
capture_it(a + b + c)
x

capture_it <- function(x) {
  enexpr(x)
}
capture_it(a + b + c)
a + b + c

表現式を捕捉できれば、それを変更・修正することが可能です。f(x = 1, y = 2)という表現式にzを追加したり、yを削除したりできます。

f <- expr(f(x = 1, y = 2))
f$z <- 3

f
f(x = 1, y = 2, z = 3)

f[[2]] <- NULL

f
f(y = 2, z = 3)

つまり表現式はリストのように[[で操作ができます。

f <- expr(f(x = 1, y = 2))
l <- as.list(f)
 
l[1]
[[1]]
f

l[[1]]
f
 
l[2]
$x
[1] 1

l[[2]]
[1] 1
 
l[3]
$y
[1] 2

l[[3]]
[1] 2

Code is tree

Rのコードはツリーのような構造を持ちます。抽象構文ツリー(AST:Abstract Syntax Tree)と呼ばれたりもする。lobstr::ast()を使えば、Rの抽象構文ツリーを表示させることができます。

lobstr::ast(f(a, "b"))
█─f 
├─a 
└─"b" 

複雑な表現式でも、抽象構文ツリーにしてしまえば構造が理解しやすいです。私みたいな人間だと()が多くなると構造が見えにくいのいつもうなだれてますが、抽象構文ツリーにできるのは大変ありがたい。

lobstr::ast(f1(f2(a, b), f3(1, f4(2))))
█─f1 
├─█─f2 
│ ├─a 
│ └─b 
└─█─f3 
  ├─1 
  └─█─f4 
    └─2 

こんな感じの戸惑うやつも、抽象構文ツリーでビジュアライズ一発で理解できます!

lobstr::ast((1 + (2 * (3/2) + 10))*2)
█─`*` 
├─█─`(` 
│ └─█─`+` 
│   ├─1 
│   └─█─`(` 
│     └─█─`+` 
│       ├─█─`*` 
│       │ ├─2 
│       │ └─█─`(` 
│       │   └─█─`/` 
│       │     ├─3 
│       │     └─2 
│       └─10 
└─2 

Code can generate code

コードがコードを生成できるとはどういうことでしょうか。call2()というを使えばコードを生成できるようです。例をみればわかると思いますが、f(1, 2, 3)という表現式をcall2(("f", 1, 2, 3))で生成している…つまり、コードがコードを生成しているということになります。

call2("f", 1, 2, 3)
f(1, 2, 3)

call2("+", 1, call2("*", 2, 3))
1 + 2 * 3

これではインタラクティブではないので、enexpr()!!(bang-bang)を使うことで同じことができるようにしたよ、Advanced R by Hadley Wickhamにはあります。expr()!!を使わないと、xx/yyを表現式として新たに捉えてしまいますが、!!を使うことで、x + xy + yを補足できています。うん、難しい。

xx <- expr(x + x)
yy <- expr(y + y)

expr(xx / yy)
xx/yy

expr(!!xx / !!yy)
(x + x)/(y + y)

Advanced R by Hadley Wickhamの例をおかりすると、enexpr()で関数に入力された表現式を補足できるようにして、その表現式を!!を使って、sd()mean()の表現式に利用しているという例です(あってるかな?)。こんな感じで要素を構築できることで、より複雑な問題を解決できるとのこと。うん、難しい。

cv <- function(var) {
  var <- enexpr(var)
  expr(sd(!!var) / mean(!!var))
}

cv(x)
sd(x)/mean(x)

cv((x * y)/2)
sd((x * y)/2)/mean((x * y)/2)

Evaluation runs code

さっきの例もそうなのですが、評価(eval)しないと計算は実行されないわけです。表現式を評価、つまり実行するためには環境が必要です。環境を適切に設定することで、式の中のシンボルが何を意味するのかをRに伝えることができます。これがまた難しいんだけど。

base::eval()は、exprenvirenclosを引数に取ることができ、それらをうまく組み合わせることで、表現式を評価します。exprは表現式、envirは環境、enclosはエンクロージング環境です。環境が指定されていないと、レキシカルスコープのルールに乗っ取り、グローバル環境にシンボルを探索しにいきます。評価環境を調整できるということは、データマスクを追加して、データフレーム内の変数があたかもその環境内の変数であるかのように参照できるようできます。

eval(expr(x + y), env(x = 1, y = 10))
[1] 11

eval(expr(x + y), envir = list(x = 1, y = 100))
[1] 101

x <- 3
y <- 1000
eval(expr(x + y))
[1] 1003

eval(expr(x + y), envir = globalenv())
[1] 1003

例として、dplyr()は、リモートデータベースで実行するためのSQLを生成する環境でコードを実行するという考え方を採用しています。

library(RSQLite)

con <- DBI::dbConnect(RSQLite::SQLite(), filename = ":memory:")
mtcars_db <- copy_to(con, mtcars)

mtcars_db %>%
  filter(cyl > 2) %>%
  select(mpg:hp) %>%
  head(10) %>%
  show_query()

<SQL>
SELECT `mpg`, `cyl`, `disp`, `hp`
FROM `mtcars`
WHERE (`cyl` > 2.0)
LIMIT 10

DBI::dbDisconnect(con)

Customising evaluation with data

環境ではなくデータフレーム内の変数を探すために評価の環境を変更する。このアイデアbase::subset()transform()ggplot2::aes()dplyr::mutate()なんかがこれに当たります。base::subset()では下記の例のように、Sepal.Length > 7を評価環境データフレーム内に変えることで、うまく評価しています。

base::subset(iris, Sepal.Length > 7)
    Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
103          7.1         3.0          5.9         2.1 virginica
106          7.6         3.0          6.6         2.1 virginica
108          7.3         2.9          6.3         1.8 virginica
110          7.2         3.6          6.1         2.5 virginica
118          7.7         3.8          6.7         2.2 virginica
119          7.7         2.6          6.9         2.3 virginica
123          7.7         2.8          6.7         2.0 virginica
126          7.2         3.2          6.0         1.8 virginica
130          7.2         3.0          5.8         1.6 virginica
131          7.4         2.8          6.1         1.9 virginica
132          7.9         3.8          6.4         2.0 virginica
136          7.7         3.0          6.1         2.3 virginica

雑に自作するとこのような感じ。

sample_df <- data.frame(a = 1:5, b = 5:1, c = c(5,3,1,4,1))
  a b c
1 1 5 5
2 2 4 3
3 3 3 1
4 4 2 4
5 5 1 1

filter_and_select <- function(data, row_cond=TRUE, col_cond=TRUE){
  # parent.frame()をeval()で設定するのは、
  # 第2引数として指定されたデータフレーム内に
  # 第1引数で指定された変数を見つけられない場合、
  # filter_and_select()の環境を調べるのを防ぎ、
  # 呼び出し元であるグローバル環境で変数を探すようにするため
  row_cond_call <- substitute(row_cond)
  row_pos <- eval(row_cond_call, data, parent.frame())
  
  col_cond_call <- substitute(col_cond)
  col_pos_tmp <- setNames(as.list(seq_along(data)), names(data))
  col_pos <- eval(col_cond_call, col_pos_tmp, parent.frame())
  
  data[row_pos, col_pos, drop = FALSE]
}


filter_and_select(data = sample_df, row_cond = a >= 3, col_cond = c(a,c))
  a c
3 3 1
4 4 4
5 5 1

eval()には、いくつか落とし穴があるそうで、rlang::eval_tidy()を代わりに使うほうが良いそうです。式や環境だけでなくeval_tidy()は、データマスクも引き受けれるそうです。eval_tidy()の環境をdfに設定することで、データフレーム内で表現式(x + y)を評価しています。データマスクを使用して評価することは、df$x + df$yではなく、x + yと書けばよい便利な手法なのですが、いくつか曖昧さが残りますが、特殊代名詞.data.envを使えばあいまいさを解消できるそうです。

df <- data.frame(x = 1:5, y = sample(5))
eval_tidy(expr(x + y), df)

[1] 5 4 8 5 8

Quosures

Quosureを下記の例を通じて学びます。Quosureというのは「表現式を評価されないままにとどめつつ、それが評価されるべき環境を覚える」というものです。はい、難しい。ちょっと理解するには時間がかかりそうです。まず、with2()という関数があったとして、現状これはうまく行っているように見えます。

df
  x y
1 1 4
2 2 2
3 3 5
4 4 1
5 5 3

with2 <- function(df, expr) {
   a <- 1000
   eval_tidy(enexpr(expr), df)
 }

with2(df, x + y)
[1] 5 4 8 5 8

しかし、with2()を下記のように少しいじると、どうなるでしょう。xの値はデータフレームからとってきて、aの値はグローバル環境から…と思ってこのコードを書くと、結果をみればわかりますが、そうはなっていません。どうやら、awith2()の環境で評価されてしまっています。

with2 <- function(df, expr) {
  a <- 1000
  eval_tidy(enexpr(expr), df)
}

df <- data.frame(x = 1:3)
a <- 10

with2(df, x + a)
[1] 1001 1002 1003

この問題は、enexpr()からenquo()を使うことで解決できます。quosureに解説があります。

enquo() takes a symbol referring to a function argument, quotes the R code that was supplied to this argument, captures the environment where the function was called (and thus where the R code was typed), and bundles them in a quosure.

enquo()は関数の引数を参照するシンボルを取り、この引数に与えられたRコードを引用符で囲み、関数が呼び出された場所(したがってRコードが入力された場所)の環境を取り込み、それらを1つの引用符で囲みます。なので、グローバル環境におりますaを取れるわけです。

with2 <- function(df, expr) {
  a <- 1000
  eval_tidy(enquo(expr), df)
}

with2(df, x + a)
[1] 11 12 13

eval_tidy()を使うときは、このようなことが起こりうるので、enexpr()ではなくenquo()を使うほうがいいとのこと。