rlang学習

Others
Author

Kentaro Kamada

Published

February 22, 2024

参考

Advanced R

下準備

library(rlang)
library(lobstr)

expression

  • expr:コードをそのまま捕まえる
    • 作成されたオブジェクト:expression
    • call, symbol, constant, pairlistの4つからなる
expr(mean(x, na.rm = TRUE))
mean(x, na.rm = TRUE)
expr(10 + 100 + 1000)
10 + 100 + 1000
  • exprは関数の引数なども書いてあるまま保持する
# これはxになる
capture_it <- function(x) {
  expr(x)
}

capture_it(a + b + c)
x
  • 引数は評価した上でコードを保持したい場合はenexprを使う
capture_it <- function(x) {
  enexpr(x)
}
capture_it(a + b + c)
a + b + c

abstract syntax tree (AST) による表現

  • codeはtree形式で表現できる
  • fがcall, aがsymbol, “b”がconstantらしい
lobstr::ast(f(a, "b"))
█─f 
├─a 
└─"b" 
  • +*も関数であることがわかる
lobstr::ast(1 + 2 * 3)
█─`+` 
├─1 
└─█─`*` 
  ├─2 
  └─3 

codeを生成するcode

  • call2
call2('f', 1, 2)
f(1, 2)
call2('+', 1, call2('*', 2, 3))
1 + 2 * 3
  • !!を使ったcodeの生成
    • 捕まえておいたコードを評価したいところで評価する
xx <- expr(x + x)
yy <- expr(y + y)

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

codeの評価

  • eval(expr, env):exprをenvで評価
eval(expr(x + y), env(x = 1, y = 10))
[1] 11
eval(expr(x + y), env(x = 2, y = 100))
[1] 102
  • envを指定しないときは現在のenvironmentで評価
x <- 10
y <- 100

eval(expr(x + y))
[1] 110
  • データを環境として扱う

  • eval_tidy(expr, data):exprをdata内部で評価

    • dataでcurrent environmentを上書きするのでdata-mask呼ばれる
  • evalでもできないことはないが、落とし穴があるらしい

df <- data.frame(x = 1:5, y = sample(5))
eval_tidy(expr(x + y), df)
[1] 2 5 8 8 7
  • dplyrっぽい関数
with2 <- function(df, expr) {
  eval_tidy(enexpr(expr), df)
}

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

Quosure

  • exprだけを使う方法は問題がでてくる
  • 関数の内部でaを定義してみる
with2 <- function(df, expr) {
  a <- 1000
  eval_tidy(enexpr(expr), df)
}
  • さらに関数の外部(global environment)でもaを定義する
df <- data.frame(x = 1:3)
a <- 10
  • この状態でwith2を使うと問題が発生
    • global environmentのaではなく、関数内部で定義したaが優先的に使われる
with2(df, x + a)
[1] 1001 1002 1003
  • quosure:expressionとenvironmentをセットで保持する
    • data-maskでは、eval_tidyのdata -> quosureのenv -> global envの順で評価される
with2 <- function(df, expr) {
  a <- 1000
  eval_tidy(enquo(expr), df)
}

x <- 10

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

Quasiquotation

library(rlang)
library(purrr)
  • pasteっぽい関数を作る
paste('Good', 'morning', 'Hadley')
[1] "Good morning Hadley"
  • 毎回’’でくくるのがだるい
cement <- function(...) {
  args <- ensyms(...) |> map_chr(as_string)
  paste(args, collapse = " ")
}

cement(Good, morning, Hadley)
[1] "Good morning Hadley"
  • morningやeveryoneが変数だったら?
    • pasteだとうまくいくけど、cementは想定したふうにはならない
time <- 'afternoon'
name <- 'Alice'

paste('Good', time, name)
[1] "Good afternoon Alice"
cement(Good, time, name)
[1] "Good time name"

!!を使って変数を評価すればうまくいく

cement(Good, !!time, !!name)
[1] "Good afternoon Alice"