R の入門記事――というよりも詰まりやすいトピック、いや正確に言うと自分が理解するのに少し躓いた物事を連ねた、半ば恨み節といっても良いような記事です。
私は今ではRを毎日のように書いていますが、使い始めた当初は「何じゃこりゃ?」と思うことの嵐でした。……いや、正直に言って今でもそうなのですが。私は R の前には C、C++、Python をそれなりに書いていて、申し訳程度に Haskell と Scheme に触ったことがあったのですが、どうも R は書いていて「あれ?」と思わされるポイントが多いように思います。もし私と同じような経験の元にRを書くことになって途方に暮れている人がいれば助けにならないかなあと思って書きました。
普通の入門であれば触れるようなこと(基本的な構文、ブロードキャスティング、よくある操作など)には触れません。また、一つ一つのトピックを掘り下げることは重視していません(でも明確に間違っている内容があったら優しく教えてください)。
変数スコープ
R のスコープは静的スコープです。関数の中から参照されるだけの変数については、関数が定義された環境での値が用いられます。関数の中での変数定義は(<<-
演算子を用いない限り)関数の外には影響を及ぼしません。以下の SO の質問についた回答はわかりやすいコード例だと思います。
Understanding lexical scoping in R
また、for
や if
節の内外ではスコープは区別されません。つまり例えば、
if (x == 1) {
y <- 1
} else {
y <- 2
}
y
としたとき、if
節の外でy
を定義していなかったとしても、y
を後から参照することが可能です。この辺りは C++ などとは違いますが、Python と大体一緒ですね。
もしそうすることが可能な処理であれば、繰り返し処理は for
よりも lapply()
を使って書く方が変数が{}
の外に染み出さず安全です。
意味がない(ように見えることが多い) .
多くの言語において演算子としての堂々たる地位を築いているはずの.
がRでは何食わぬ顔で名前を構成する文字として使われており、これはRのコードを初めて見た時に目を見開きそうになるポイントの一つだと思います。data.frame
はdata
というモジュールの中のframe
クラスなの? row.names
はrow
というオブジェクトのメンバなの?……あ、そうじゃないんだ、へぇ……。
.
の使い方についてはSOに投げられた以下の質問のベストアンサーがよくまとまっています。
[What does the dot mean in R – personal preference, naming convention or more?]
(http://stackoverflow.com/questions/7526467/what-does-the-dot-mean-in-r-personal-preference-naming-convention-or-more)
詳細はリンク先を読んでいただくとして要点だけ抜き出すと、Rで .
を使うケースとしては以下のいずれかです。
- 特に意味はなく単なる単語区切り文字
- S3 クラスのメソッド名とクラス名を分離する
-
ls()
の結果からオブジェクトを隠す - その他、関数の引数名が可変な関数で名前がぶつかるのを避ける、など
R ではたくさんのメソッドやらなにやらで.
が使われている一方、標準パッケージでは _
を見かけないものですから、単語区切りに.
を使うのが R での推奨スタイルなのかなと思ってしまいます。でも、むしろ 2. の S3 メソッドとして使われているケースとの混乱を防ぐために.
を使うのは避けた方が良いくらいです(Hadley のコーディングスタイルガイドにはそうあります)。例えばあなたがもし不用意にas.list.data.frame()
に適当な関数を定義してしまった場合(そんなことが起きるのか?)、data.frame
クラスのオブジェクトを引数に取るas.list()
は出鱈目な結果を返すようになり、阿鼻叫喚の事態となることでしょう……。
ジェネリック関数
前項で S3 クラスについて触れましたが、そう、Rでは引数のクラスに応じて同じ呼び出しで挙動が変わるジェネリックな関数がサポートされています。実際には型チェックに基づいた分岐を少し簡単に書くための構文が準備されていると言った方が正確です。
正直、以下のようなことが楽勝で出来てしまうあたり後付けな感じはしてしまいます。
class(iris) <- "foo"
as.list.foo <- function (x) print("This is a generic function!!!")
as.list(iris)
# [1] "This is a generic function!!!"
確かにこういった形でのジェネリック関数のサポートは良いところもありますが(何やらようわからんオブジェクトでも head()
やら tail()
やら summary()
やらを標準でサポートされているものと同じように呼び出せたり)、通常の関数とこのようなジェネリック関数との両方があることは把握している必要があります。
関数の assignment form
オブジェクトの属性を取ってくるような関数の返り値に何かを代入する、という以下のような書き方それ自体は他の言語でも生じうることかとは思います。
x <- data.frame(a = c(1,2,3), b = c(2,3,4))
names(x) <- c("A", "B")
この書き方から受けるファーストインプレッションは、names(x)
で参照を渡されたオブジェクトに対する代入操作が行われているのだろうということです。そう思った人は、きっと次のコードでもx
の列名変更が行われるはずだと思いますよね?(私だけでしょうか?)
x <- data.frame(a = c(1,2,3), b = c(2,3,4))
x_names <- names(x)
x_names <- c("A", "B")
でもこれをやったあとでx
の列名は書き換わりません。
ここでのからくりは、実はnames
とnames<-
という二つの関数が定義されており、names(x)
に対する代入文は後者の返り値をx
に代入するシンタックスシュガーだということです。
オブジェクトの不変性
前項のようなシンタックスシュガーが用意されている理由の一つでもあるのでしょうけれど、ほとんどの R のオブジェクトは不変、つまり書き換え不可能です。「嘘……だろ……?」と思いませんでした? 詳しいことはUsing closures as objects in R -- Win-Vector Blogを読んでいただきたいです。要するに R においては、オブジェクトの一部を操作しているかのように見える文でも、実際に行われていることは「操作対象のオブジェクトの一部を変更したあとのオブジェクトを作成する+名前が参照するオブジェクトのアドレスをそちらに変更する」である、ということです。
私は R を書き始めた頃に参照渡しなのか値渡しなのかでひどく混乱した記憶があります。値渡しをしているとした場合に起きるはずのオブジェクトのサイズによる関数呼び出しのパフォーマンス変化はないし、しかし関数内部でのオブジェクトに対する破壊的操作(に見えるもの)が無かったことにされているのを見ると参照渡しでもないのか……? と思わされたのですが、それはこのようなトリックが知らないうちに使われていることにも一因を発していました。
遅延評価
Rでは関数の引数として与えられた表現式は呼び出し先で参照された場合に初めて評価されます。次のようなコードを実行してみれば一目瞭然かと思われます。
f1 <- function () print("f1")
f2 <- function (x) {
print("f2")
return(x)
}
f2(f1())
# 結果は
# [1] "f2"
# [1] "f1"
例えば例外処理が専用構文を用意せずともtryCatch()
という関数によって実現できているのはそのおかげですし、トップレベルで呼び出しているつもりの関数がなぜかスタックトレースの上の方に乗っかっていたりすることがあるのもそこに起因します。また例えば、もしある関数に引数として渡された表現式がエラーを起こすようなものであったとしても、それが評価されないコードパスを通っている限りは何も起きません。遅延評価によって基本的には無駄な計算量が削減できますが、場合によっては、もっと早い段階でエラー出してくれよってこともあるでしょう。
Non Standard Evaluation (NSE)
R では表現式の解析木を取得するsubstitute()
、好きな環境で解析木を評価する eval()
、文字列データを解析木にする parse()
などといった関数をカジュアルに利用することができます。
前項で述べたように R では関数を呼び出すときに引数として渡された表現式の評価は後回しにされるのですが、上記のような関数群を使えばその表現式の解析木と評価手段に自在に介入することが可能になるのです。このように通常の評価とは外れた方法による評価が NSE と呼ばれるものです。
NSE はパッケージ作者にとっては R を強力な DSL 作成言語たらしめる力の源でもあるのですが、それはとりもなおさず初見殺しな表現の源でもあるということです。
magrittr
やdplyr
のような強烈なパッケージを引き合いに出さなくても、base パッケージに入っている with()
や subset()
だって十分に黒魔術的だと思います。
subset(iris, Species == "setosa")
のようなコードを見た時、他言語から入ってきた R 初心者の中にはクオートされていない Species
に不安を覚える人も多いのではないでしょうか? ここでのトリックとしてはsubset()
関数の中で第二引数の表現文を、環境をそっと挿げ替えたうえで評価している訳です。
ただ、このあたりの感覚はたくさんRを書いているうちにきっとそのうち麻痺してきて、タイプ数が少なくて楽ちんじゃんとか思うようになってきます。恐ろしいことです。
あとがき
如何だったでしょうか? これは別に難しくないでしょとか、もっとややこしいあれについて書けよとか色々あったかもしれません。
R には理解が難しい機能がたくさんあるというほどではないと思いますが、手続き型のスクリプト言語でしょって思って深く考えずに書くと意外なところで穴にはまることが多いなあという印象があります。