R言語: 関数で可変長引数を扱う(評価あり・なし)
R言語の関数で可変長引数を扱う(評価あり・なし)
R言語の関数で、可変長引数を扱う方法です。 備忘録として。
Ellipses ( ... )
Rのドキュメントをみていると、関数の引数のところに、3連のドット(ピリオド)が書いてあるものがあります。
例) https://stat.ethz.ch/R-manual/R-patched/library/graphics/html/plot.default.html
これは可変長引数が扱える関数です。この ... がついた関数は、明示されている以上の数の引数をとることができます。
この記号、名前は、ellipsis (英語で省略記法の意味だそうです。)というそうです。 複数形になって、ellipses と書かれていたりします。
後述のように、そのまま、... を他の関数に渡したり、list() 関数を適用してlistオブジェクトに変換することで、可変長引数が扱いやすくなります。この場合、引数のExpressionが、評価されたものが、リストの要素に入ります。
func_wrapper = function( ... ){
list(...)
}
func_wrapper( x = 11 + 22)
# $x
# [1] 33
この記事の最後で、評価しないで、リストの要素に入れる方法も紹介します。 参考に、先にサンプルコードだけ示しておきます。
func_wrapper2 = function( ... ){
eval(substitute(alist(...)))
}
func_wrapper2( x = 11 + 22)
# $x
# 11 + 22
Ellipses の使用例 (評価あり)
代表的には以下の2つでしょうか。1. 可変長引数を取る他の関数にそのまま渡したい。 2.自作関数でユーザの渡した引数をチェックしたい。
1. 可変長引数を取る他の関数にそのまま渡したい
以下の例では、plot_wrapper() に渡された引数をすべて、plot()関数に渡します。 仮引数の ... という記法が、そのまま、次の関数呼び出しにも使えて、わかりやすいですね。
plot_wrapper = function( ... ){
cat("call plot() \n")
plot( ... )
}
plot_wrapper( x = c(1,2,3), y = c(1,8,27) )
2. 自作関数でユーザの渡した引数をチェックしたい
まず、ポイントとして、仮引数の、 ... を、扱いやすいような形に変換するには、list() を使えばよいそうです。
以下の例では、arglist に、list( x = c(1,2,3), y = c(1,8,27) , main = "x vs y" ) が渡ることになるので、names() を適用することで、引数の名前が取り出せて(本例では、c("x", "y", "main") )、ここに、"main"が含まれるか判定して、含まれれば、それをそのまま使う。含まれなければ、plot()を呼ぶときに、 main = "scatter plot" 引数を追加する、というプログラムになります。
plot_wrapper = function( ... ){
arglist = list(...)
if( "main" %in% names(arglist) ) {
plot( ... )
} else {
plot( ... , main = "scatter plot" )
}
}
# plot_wrapper( x = c(1,2,3), y = c(1,8,27) )
plot_wrapper( x = c(1,2,3), y = c(1,8,27) , main = "x vs y")
補足
この ellipses はどういう風に扱われているのかなと思っていたのですが、Rとしてはシンボルの一つで、内部的には、どうも、DOTSXP でよいようです。
可変長引数を評価しないで扱う (評価無し)
可変長引数を評価しないで、Expressionが要素のリストに入れるという方法もあります。
このテクニックを使うと、可変長引数をExpression(LANGSXP, symbol, atomic type)が要素のリストに入れることができます。このような使い方は、poormanというbase Rのみを使ってdplyrのクローンを作っているパッケージの中で、dotdotdot()関数の実装の中で見かけました。
このpoormanの関数ではdotdotdot()という関数が、 ほとんどの関数の...を処理しています。 dotdotdot()は、dots.R で定義されている。
ここをみると、eval(substitute(alist(...))) という処理が初めにおこなわれており、どういう処理をしているのか、以下のように追ってみました。
eval()は、引数のAST(LANGSXP)を評価します。
substitute()は、引数をAST(LANGSXP)として返しますが、今回の引数に来るASTをlobstrパッケージを使って表示すると以下のようになります。
test2 = function(...){
lobstr::ast( alist(...))
}
test2( x + y , 11 )
█─alist
└─...
このASTが、eval() で評価されると、... の部分が、x + y のLANGSXPと、11というatomic typeに評価されて、それらを引数に、alist がリストを作ります。
なので、最終的には、リストで、要素が、x+yというAST(LANGSXP)と、11というAST(atomic typeのみ)で構成されたものが返されます。
なお、
alist(...) だけでも、同じようなASTを要素としたリストができるのではないか?と思いますが、実際に実行してみると、ASTを持ったリストではあるのですが、その要素は、...という(symbolのみの)ASTが返されてしまいます。
というわけで、eval(substitute(alist(...))) はイディオム的に使えそうですね。
個人的メモ
Lisperにとっては普通なのかもしれませんが、このsubsitute()という関数は特殊で、このような引数をASTのまま扱う関数があると、関数の内側から外側に読ん(今回だと、alist(...)からevalの方向に読む)で、内側から外側に評価されると理解するとおかしなことになります。 substitute()より内側は、一旦、評価されないで、ASTのまま扱われるからです。
つまり、Lispの場合、関数の外側から内側にみて、このようなsubstitute()のように引数をASTのまま扱うような関数にくると、そこから外側に戻るよう読むとよさそうです。普通のプログラミング言語は、内側から外側に読んで評価すればよいですが、Lisp語族は、1.外側から内側へ 2.内側から外側へという順番で読むと良さそうです。
Discussion