論理の流刑地

地獄の底を、爆笑しながら闊歩する

【小ネタ続報】dplyr系の関数を自作関数内で用いるときにはquasiquotationを利用する

セブンの麻婆飯うめぇ。

Introduction

ちょいと前にこんな記事を書いた。
ronri-rukeichi.hatenablog.com

自分でNSEな感じで変数を渡して、内部でdplyr系の関数(group_byとかfilterとかsummarizeとか)を使うために、
あんまりsmartな方法が思いつかず、eval()を使うとてもみっともない方法をとるしかなかったのだが、小野滋氏という神が愚鈍な後進のために素晴らしい記事を書いてくださっていたのを発見した。

elsur.jpn.org

どうやら、quosiquotationという仕組みをつかうとうまくやれるらしい。なるほどねー。

〈参考URL〉

  1. quasiquotation function | R Documentation
  2. http://rtokei.tech/r/tidyeval%E3%81%AE%E3%81%93%E3%81%A8%E3%82%92%E5%AD%A6%E3%81%B6/
  3. 19 Quasiquotation | Advanced R Solutions(Hadley神の記事)

2つめの日本語の記事がわかりやすかった。ガチの神記事。
なんか全般的にtidyevalまわりについて勉強したほうがいい気がする*1

この記事に書いてある、

Qousureは表現を評価されないままにしながら、評価されるべき環境を覚えさせる方法

というのが要諦である。

使用例1

たとえばデータフレームから水準を複数(1つ以上)指定して、
その複数の水準ごとに、平均を返してくれるような関数を考えると、以下のように実装すればよい

get_grpM <- function( dta , y , ...){
  grp_v <-  quos(...)
  gby <- dplyr::group_by( dta, !!!grp_v)
  enq_y <-  enquo(y)
  return(dplyr::summarize( gby , y = mean(!!(enq_y),na.rm=T)))
}

#実行
get_grpM( testData , income , age , sex )

ポイントは以下。

  • quos()で受け取った水準を表す変数名を、dplyr::group_byのなかでは!!!演算子で再評価している
  • 平均をとる変数はenquo()で受け取って、dplyr::summarize内では!!演算子で再評価している

使用例2

ちなみに前の記事でやった、関数内で生成した文字列を表現型にするみたいな処理は、sym()やsyms()を使いつつも、いったんquosure化を挟むと話がわかりやすくなる

df1 <- data.frame(x1= 1:5 , x2= 2:6, x5=10:6)

testFun <- function(dta , var){
  vn1 <- (rlang::parse_expr(paste0(var,5)))
  quo_vn1 <- quo(!!vn1)
   return( dplyr::select(dta,!!quo_vn1 ))
} #testFun

# > testFun(df1, "x")
# x5
#  10
#   9
#   8
#   7
#   6
  • rlang::parse_expr()により、作った文字列を表現式(expression)に変換
  • expressionからquosureを作るときには、quo()内で!!演算子を用いることによって表現式のほうを評価させる
  • dplyr::selectの中でも!!演算子を使って、quosureを評価した結果(表現式)のほうを関数にわたす(これはもはや基本)

いや、eval()とか使った小細工よりはだいぶシンプルになったけどまだわかりにくいか....
でもまだスマートな感じはする。

【おまけ】気持ち悪いところ:quo()とquos()は並列関係にない

自分のなかで気持ち悪い書き方をしないと実装できなかった部分をquosureの利用により克服できたのはわりと爽快感がある。
しかし、一か所気持ち悪いところがあった。その点を順を追って書き留めておく。

quo() vs enquo()

要素のquosureを作るには主に二つの関数があって、それはquo()とenquo()である。
quosureの構成要素は表現式(expr)と環境(env)であるが、関数内で使われるときにこの二つの構成要素の解釈の仕方がquo()とenquo()では異なってくる。

quo()では実行されているその環境をenvにとり、渡された引数をそのままexprに設定する。
enquo()では関数の呼び出し元の環境をenvとし、その呼び出し元の環境で引数を評価したものをexprに設定する。

言葉で説明しても分かりにくいので、コードで示すと以下のような感じである。

fun1 <- function(x) quo(x)
fun2 <- function(x) enquo(x)

> fun1(abc)
# <quosure>
#   expr: ^x
# env:  00000000311629A8

> fun2(abc)
# <quosure>
#   expr: ^abc
# env:  global

quoでは表現式は関数内での表現(x)かつ環境は関数内環境に、enquoでは表現式は関数に渡された表現(abc)かつ環境はglobal環境になっている。
ここまでがとりあえず前提知識。

quos()が対応するのはquo()ではなくenquo()なのか?

さてここでquos()という関数がある。さきの小野さんの記事内から引用すると、

quos()は...をquosureのリストにして返す。
!!!はquosureのリストをアンクオートしてつないでくれる(これをunquote-splicingという)

ここで...は、関数定義のさい任意の数の引数を受け付けるためにつかうやつである。先の使用例1でも用いていた。

quos()という字面的にquo()の複数版のように思えるので、環境は関数内環境が使われるのかと予想するのが自然であろう。
しかし、これは呼び出し元の環境が用いられるようである。

fun3 <- function(...) quos(...)

> fun3(a,b,c)
# <list_of<quosure>>
#   
#   [[1]]
# <quosure>
#   expr: ^a
# env:  global
# 
# [[2]]
# <quosure>
#   expr: ^b
# env:  global
# 
# [[3]]
# <quosure>
#   expr: ^c
# env:  global

このように、envにはglobal環境が入っている。これはquo()よりもenquo()に近い
ここが若干混乱しやすいし何故こうなってるのか調べてもわからない*2ので、一応備忘のために記録しておく。

まぁ便利ではあるからいいか(いいのか?)

Enjoy!!

*1:まぁ仕事に直接結びつくわけでないのでどれだけ時間をかけるかは難しいのだが

*2:さらにはenquos()という関数まである