🐢

R言語: 関数で可変長引数を扱う(評価あり・なし)

2021/01/24に公開

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 で定義されている。

https://github.com/nathaneastwood/poorman/blob/master/R/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