通常評価版の関数(SE)と非通常評価版の関数(NSE)を両方つくっといた方が分かりやすい気がした

もうちょっとあとでブログに書くchartistパッケージをつくったときのメモ。

NSEって?

NSEの説明は私の手には負えないので、他の資料に丸投げします。この辺を読めばばっちりです。

Non Standard Evaluation (NSE)とは、関数内部から、その関数を呼び出した時の引数の値じゃなくて表現式そのものを、 関数の中での処理に利用しようという引数評価の方法です。(R - NSEとは何か - Qiita

NSEはインタラクティブにデータをいじる時には便利だけどプログラミング(インタラクティブの対義語的なイメージ)する時には色々困るので、そういうときはSEを使おうぜ、ということらしい。(NSE(Non-Standard Evaluation)について - 東京で尻を洗う

In most programming languages, you can only access the values of a function’s arguments. In R, you can also access the code used to compute them. This makes it possible to evaluate code in non-standard ways: to use what is known as non-standard evaluation, or NSE for short. NSE is particularly useful for functions when doing interactive data analysis because it can dramatically reduce the amount of typing. (Non-standard evaluation · Advanced R.

ただ、一番上の資料に関しては、シリアルパッケージクリエーターとして著名なはずの@dichikaさんが、

パッケージユーザーであり、特に開発にも関わらない私のような人間が

とか書いていて別人なのでは?疑惑もあるので注意が必要です。

簡単な例として、subset()もどきの関数をつくってみます。data.frameと列名を引数として取り、指定された列のみを返します。NSEの方は厳めしい感じになってしまっています。。もっと簡単な書き方をご存知の方は教えてください。

# SE
subset_SE = function(d, ...) {
  col <- c(...)
  print(col)
  d[, col]
}

# NSE
subset_NSE = function(d, ...) {
  # eval(substitute(alist(...)))は、pryr::dots(...)と同じです
  col <- sapply(eval(substitute(alist(...))), deparse)
  print(col)
  d[, col]
}

では、これを使ってみます。

d <- data.frame(X=1:3, Y=LETTERS[1:3], Z=paste0("test",1:3))

subset_SE(d, "X")     #これはできる
#> [1] "X"
#> [1] 1 2 3
subset_SE(d, X)       #これはできない
#> Error in `[.data.frame`(d, , col) : object 'X' not found

subset_NSE(d, "X")     #これはできる
#> [1] 1 2 3
subset_NSE(d, X)       #これもできる
#> [1] 1 2 3

これだけ見ると、" "をつけなくてもいいのでNSEのほうがよくない?と思いますが(※本家subsetは""がついててもついてなくても動きます)、 NSE版は、ベクトルを渡したり変数を渡したりすることができません。

foo <- "X"

subset_SE(d, c("X", "Y"))  #これはできる
#> [1] "X" "Y"
#>   X Y
#> 1 1 A
#> 2 2 B
#> 3 3 C
subset_SE(d, foo)            #これはできる
#> [1] "X"
#> [1] 1 2 3

subset_NSE(d, c("X", "Y"))    #これはできない
#> [1] "c(\"X\", \"Y\")"
#>   Error in `[.data.frame`(d, , col) : undefined columns selected
subset_NSE(d, foo)            #これはできない
#> [1] "foo"
#>   Error in `[.data.frame`(d, , col) : undefined columns selecte

これは、私のNSE版の実装では受け取った引数を問答無用で文字列にしてしまうからです。もちろん、がんばれば色んなパターンに対応させることができますが、激しくめんどくさいです。もうちょっと楽な人生を歩みたいです。

ではどうするかというと、めんどくさくても別々に作っといた方が便利です。

例:tidyr::gather()

dplyrとかtidyrの関数には、だいたいSE版とNSE版が両方用意されていて、SE版の方には後ろに_が付くようになっています。ここで例としてtidyrのgather()gather_()のコードを見てみましょう。

gather()(NSE)は、

stocks <- data.frame(
  time = as.Date('2009-01-01') + 0:9,
  X = rnorm(10, 0, 1),
  Y = rnorm(10, 0, 2),
  Z = rnorm(10, 0, 4)
)

gather(stocks, stock, price, -time)

のように指定する方で、gather_()(SE)は、

gather_(stocks, "stock", "price", c("X", "Y", "Z"))

のように指定する方です。

コード読んでみる

gather()のコードはこんな感じです。

gather <- function(data, key, value, ..., na.rm = FALSE, convert = FALSE) {
  key_col <- col_name(substitute(key), "key")
  value_col <- col_name(substitute(value), "value")

  if (n_dots(...) == 0) {
    gather_cols <- setdiff(names(data), c(key_col, value_col))
  } else {
    gather_cols <- unname(dplyr::select_vars(names(data), ...))
  }


  gather_(data, key_col, value_col, gather_cols, na.rm = na.rm,
    convert = convert)
}

ここでやってることは何かというと、

1) ...(elipses)で指定した列がなんなのか割り出して 2) SE版の関数gather_()に渡す

ということだけです。ほとんどの処理はgather_()の方に記述されています。

こんな感じで、実際の処理を実装するのはSE版の方で、NSE版はそれのラッパーとしてつくる、という感じが見通しがいいなと思いました。

結果、書いたコードはこんな風になりました。...の処理はよく分からなかったので、gather_()でも使われていたdplyr::select_vars()に丸投げです:P

chartist/chartist.R at 032fb6b5d35e575f9dd4a41f609438f519ac012d · yutannihilation/chartist · GitHub