🦕

R言語のexpressionについて(Rレベル・Cレベルで)

8 min read

R言語のexpressionについて(Rレベル・Cレベルで)

Rの内部構造について、ここ数か月、ドキュメントをいろいろ読んでいて、expressionという表現でかなりひっかかったので、備忘録も兼ねて、理解していることを記事にしました。

Rスクリプトというより、プログラミング言語そのものにかかわってくる話で、abstract syntax tree と言われて、何のことかわかる方が対象になります。

なお、個人の理解に基づいた記事ですので、間違いや不正確さあるかもしれません。

Rのexpressionから、abstract syntax treeまで

Rは mean(abs(x)) というように書くと、x (ベクトル)の、絶対値をとって、その平均を、長さ1のベクトルで返します。 このような括弧()の連鎖で書けて、値を返すようなものを、Rレベルでexpression(式)と呼びます。このようなRのexpressionは、内部的には、LispのS式のような構造、つまり、CARやCDRの組み合わせでアクセスできるような構造のAbstract Syntax Tree(AST)に変換されて、評価(実行)されます。

このASTの構造は、例えば、lobstr パッケージを入れて、lobstr::ast( mean(abs(c(-1,-2,-3))) ) などとしてみると、mean(abs(c(-1,-2,-3))) というのが、どういう構造に変換されているか知ることができます。 以下は実行例です。

> library(lobstr)
> lobstr::ast( mean(abs(c(-1,-2,-3))) )
█─mean 
└─█─abs 
  └─█─c 
    ├─█─`-` 
    │ └─1 
    ├─█─`-` 
    │ └─2 
    └─█─`-` 
      └─3 

LispやS式を知っている方であれば、この構造を評価すれば、mean(abs(c(-1,-2,-3))) のexpressionの評価ができることが分かると思います。

LANGSXP とは

Rレベルの expressionが、ASTに変換されると、そのASTは、関数を表現するS式様の構造や、シンボル(変数)や、atomic vector typeのオブジェクトで構成されています。この関数を表現する構造としては、内部ではLANGSXP という型で使われています。S式様と書いたのは、その構造のCARやCDRの部分に、別のLANGSXPが入ったり、シンボルが来たり、atomic vector typeのオブジェクトが来ることでき、関数を表現しているからです。 最終的に、このAST全体を評価して、値を返しています。前述の、lobstr::ast( mean(abs(c(-1,-2,-3))) )の実行結果の中で、█ で表現されている部分が、LANGSXP の部分のようです。

EXPRSXP とは

上記のように、Rにおいてのexpressionを説明しました (実はドキュメント上はexpressions(複数形)もexpressionと書いてあったりますが、それはもう少し後で触れます)。そして、そのexpressionが、LANGSXPや、シンボル(変数)や、atomic vector typeのオブジェクトで構成されたASTに変換されて、評価されるということを書きました。

それでは、Rレベルでのexpressionの羅列は、どのように、内部的に表現されるのでしょうか。 つまり、以下のコードのように、-1,-2,-3 の平均を計算したあとに、-6,-4,-2 の平均も計算したいような場合です。

mean(abs(c(-1,-2,-3)))
mean(abs(c(-6,-4,-2)))

これは、それぞれのExpressionに対応したLANGSXPの並んだ、(Cでの可変長)配列のような構造で扱われます。 この配列は、EXPRSXPという型名の型で扱われます (※1)。 一つのExpressionにはLANGSXPという型名、複数のExpressionsにはEXPRSXPという型名が対応しています。ネーミングがややこしいですね。

つまり

Rスクリプト => Cの実装
Expression => LANGSXPやシンボル(変数)やAtomic vector typesによるAST
Expressions => EXPRSXP

というように変換されるようです。

(なお、私の覚え方は、英語の話で、expression の羅列を、expressions(複数形)と呼べると思いますので、それに因んで、このexpressionの羅列、expressionsを、内部的には、EXPRSXP という型名にしていると、覚えています。)

※1 実際に、Cのソースを見たわけでなく、R Internalsのドキュメントの、1.1.1 SEXPTYPEsに、"Expressions are of type EXPRSXP: they are a vector of (usually language) objects most often seen as the result of parse(). " と書かれています。 また、Footnote のところに、" ‘growable’ vectors (atomic vectors, VECSXPs and EXPRSXPs) " という表現もあり、やはり、EXPRSXPは可変長配列のような構造であると思われます。

R documentation の中での expression の意味

Rはユーザが利用できるすべての関数について、標準添付のパッケージは当然のことながら、CRANに登録されるパッケージの関数にも、ドキュメントを付けることが義務付けられています。基本的には、これは、Rスクリプトを書くユーザのためのドキュメントなので、Rレベルの用語のはずです。

なので、expression と言われれば、上記のように、 mean(abs(c(-1,-2,-3))) のような、その後、ASTに変換されて評価できるものを指します。さらに、その構成要素になりうるシンボル(例えば変数)も、評価して値を返すので、Rレベルのexpressionが指すものに含まれます。

注意点として、ドキュメントでexpression(不可算として扱っているのか単数として書いているのかは不明ですが)と書かれている場合に、上記のEXPRSXPを指す"こともある"点には注意が必要と思われます。(要は、私の理解で expressions (=EXPRSXP) と書いたものが、expression とドキュメントで表現されていることがあります。 後述の str2expression() and parse() を参照 )

このことは、R Internalsのドキュメントの、1.1.3 The ‘data’の項目に、記載されています。

LANGSXP
    A special type of LISTSXP used for function calls. (The CAR references the function (perhaps via a symbol or language object), and the CDR the argument list with tags for named arguments.) R-level documentation references to ‘expressions’ / ‘language objects’ are mainly LANGSXPs, but can be symbols (SYMSXPs) or expression vectors (EXPRSXPs).

R documentation の language の意味

Rのドキュメントを読んでいると、language と書いてあることがあります。 これは、内部表現のLANGSXPを意識した表現と思われますが、expression と同じことになります。 上記のように、R Internalsのドキュメントの、1.1.3 The ‘data’の項目に、記載されています。

実例

上記のように、expression(LANGSXPなど)、expressions(EXPRSXP)についてみましたが、それを理解すると、以下の挙動や、名前も理解しやすいのかなと思います。

lobstr::ast() の引数は、expression

lobstr::ast() の引数は、expression と記載されています。 実際、

lobstr::ast( mean(x) )  # OK ちなみに、lobstr::ast( x = mean(x) ) とも書けます。 
lobstr::ast( mean(x); mean(y)) # Error 

エラーの理由は、expressionはよいが、expressionsはダメのようです。

str2lang(), str2expression() and parse()

str2langや、str2expression という関数があります。 ( https://stat.ethz.ch/R-manual/R-devel/library/base/html/parse.html )

名前からも想像できますが、string(文字列)を、langや、expression に変換します。

この、str2lang(), str2expression()の、langとexpression という名前の部分は、内部実装を意識したものと思われ、それぞれ、Stringを、LANGSXPと、EXPRSXPに変換するという意味のようです。 (後者をRレベルでのexpressionと想像すると、どちらもLANGSXPに変換するように勘違いしてしまいます。)

因みに、前者は、; を含む、複数の式を記述したような文字列はエラーになりますが、後者はOKです。 返す結果も異なります。

この仲間の関数に、parse() があります。 ドキュメントには、expression を返すとありますが、expressions (EXPRSXP) を返します。

> str2lang( "mean(x); mean(y)")
Error in str2lang(s) : parsing result not of length one, but 2

> str2expression( "mean(x); mean(y)")
expression(mean(x), mean(y))

> parse( text = "mean(x); mean(y)")
expression(mean(x), mean(y))

なお、quote() というものは、str2langのように、LANGSXP を返すのですが、引数には expression をとります。つまりいかのようです。

> quote( mean(x) )

(ref.) https://stat.ethz.ch/R-manual/R-devel/library/base/html/substitute.html

deparse() は expression を引数にとる

deparse() の挙動をみると、分かったように思っていた、expression が分からなくなるかもしれません。 deparse()のドキュメントをみると、'These functions turn unevaluated expressions (where ‘expression’ is taken in a wider sense than the strict concept of a vector of mode and type (typeof) "expression" used in expression) into character strings' と書いてあって、未評価のexpression (おそらくLANGSXP相当)を、文字列に変換するとかいてあります。

(ref) https://stat.ethz.ch/R-manual/R-devel/library/base/html/deparse.html

実行してみます。

deparse( expr = mean(c(1,2,3)) )
# [1] "2"

"mean(c(1,2,3))" のようなものが返ってくるのかと思うと、"2" という文字列が返ってきました。ちなみに、"mean(c(1,2,3))"を返そうとすると、以下のようにします。

> deparse( expr = quote( mean(c(1,2,3)) ))
[1] "mean(c(1, 2, 3))"

このあたりの挙動はわかりにくいですね。

なぜ、このような挙動が生じるかというと、Rのような、Lisp・Scheme系の言語では、関数の引数が、ASTのまま渡されているからだと思われます。( Lisp・Scheme系以外の言語は、関数の引数は、値やオブジェクトという形の、ASTを評価した結果で渡されるのが普通だと思います。)

関数のユーザからみると、上述の、lobstr::ast() は、expression をASTのまま扱っていて、deparse()は、expressionを評価したうえで、ASTとして扱っているように見えます。しかし、どちらも、ASTとして関数に渡されていて、それを評価したり、評価しないままのASTで扱ったりというのは、関数の実装者側が行っています。 lobstr::ast() のドキュメントをみると、'An expression to display. Input is automatically quoted, use !! to unquote if you have already captured an expression object.' と、自動的にquoteしていますと、書いてあります。

(ref.) https://www.rdocumentation.org/packages/lobstr/versions/1.1.1/topics/ast

理解を深めるためにmean()を例にとると、mean(c(1,2,3)) のような場合、mean()には、c(1,2,3)の結果としての[1,2,3]のような構造が渡されているではなく、c()関数と引数1,2,3 という AST構造を渡しているだけです。 そう考えると、mean() という関数のドキュメントも、expression を渡してくださいと書いてもよさそうですが、そうすると、すべての関数のドキュメントが引数 expression になってしまいます。そのため、普通、mean()のドキュメントは 'An R object. Currently there are methods for numeric/logical vectors and date, date-time and time interval objects. Complex vectors are allowed for trim = 0, only.' というように、numeric/logical vectors などを引数に渡してください、というように評価した後の値を書いています。 ただ、ASTを評価した結果 numeric/logical vectors を返す expression を渡してくださいと書いてもよいのだと思われます。

(ref.) https://stat.ethz.ch/R-manual/R-devel/library/base/html/mean.html

deparse()の話に戻りますが、おそらく、関数の中で渡された引数を、quote() しないで、そのまま使うため、一回評価されてしまうのでしょう。 ドキュメント的には、expression を渡してくださいと書いてあっても、quote()したものを渡すのか、quote()は内部的に行われるのかは、わかりやすく書いてあることもあるし、書いてないこともあるようですので、注意が必要と思われます。

※ なお、関数の引数にASTをそのまま渡すと書いていますが、もう少し正確には、関数+外部の環境をPromise(PROMSXP)という型で渡しているようです。そうすることで、関数+外部の環境を評価すれば、値を引数に渡したように見える仕組みのようです。(後述)

落ち穂拾い

Rレベルで関数(LANGSXP)に見えるもの

R レベルで関数と同じように書くのだけど、内部的には、異なった実装のものに、SPECIALSXP や BUILTINSXP があります。おそらく、R以外のCなどで実装したものが、これらの型で表現されるようです。

R Internalsのドキュメントの、1.1.1 SEXPTYPEs からの抜粋になりますが、以下です。

6	LANGSXP	language objects
7	SPECIALSXP	special functions
8	BUILTINSXP	builtin functions

Rの関数の仮引数について

Rの関数の仮引数には、実際は、Promise (内部の型は、PROMSXP) というオブジェクトが渡されるようで、ここには、実引数に渡された、LANGSXPなどで構成されたASTが含まれているようです。つまり、mean(abs(x))のように、abs(x)をmean()に渡すときに、abs(x)の評価結果を渡しているのでなく、abs(x)をASTとして渡しているんですね。

このAST部分だけをPromiseオブジェクトから取り出す方法があります。 quote() や substitute() という関数を使うと、ASTを取り出して扱うことができます。

さらに話を進めていくと、Rで特に最近よく使われている、non standard evaluation という話につなげていけるのだと思いますが、奥が深いですね。

最後に

ドキュメントを読んでいて、expression, language など かなりわかりにくかったので、まとめてみました。おそらく、SchemeやLispを分かっていると、しっくりくるのかなと思いました。