関数は、Basicsの概念で、次のような例とともに紹介しました。
squareit <- function(x) {
x * x
}
squareit(3)
#> [1] 9
# shorter form
squareit_short <- function(x) x ^ 2
squareitの定義をよく見ると、いくつかの部分を識別できます。
xを受け取ります。{ }で囲まれた関数本体があります。squareitに代入されます。Rでは、関数は数値や文字列と同じように、第一級オブジェクトです。
したがって、squareit <- function...は、構文的にはx <- 42とまったく同じ代入です。
Rは、PythonやJuliaのような他のスクリプト言語とは異なり、位置引数とキーワード引数を明確に区別しません。
関数呼び出しでは、値を位置で渡すことも、名前で渡すこともできます。 後者は、引数が多く、その順番を覚えるのが難しい複雑な関数で役立ちます。
f <- function(x, y) x / y
# call positionally
f(4, 2)
#> [1] 2
# call by name
f(y = 2, x = 4)
#> [1] 2
デフォルトの引数の値は関数定義で指定できますが、デフォルトを持たないすべての引数のあとに置く必要があります。
そうすれば、デフォルトを受け入れるか、上書きするかを選べます。
g <- function(x, y = 2) x / y
# default y value
g(6)
#> [1] 3
# explicit y value
g(6, 3)
#> [1] 2
任意の数の追加引数を受け取るには、定義で...(省略記号)を使います。
関数呼び出しの余分な値をベクトルに変換することもできますが、これらの「ドット引数」(他のいくつかの言語では「varargs」と呼ばれます)の別の使い方については、この先を読んでください。
var_f <- function(x, y, ...) {
print(c(...))
}
var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"
これまでは、仮引数と本体がともに関数の構成要素だと述べてきました。
実際には3つ目の構成要素があります。関数が定義された環境です。
これは入れ子の関数の場合で説明できます。
outer_func <- function(x) {
inner_func <- function(y) {
x * y
}
inner_func(3)
}
outer_func(5)
#> [1] 15
関数呼び出しはx = 5を外側の関数に渡し、この値はその関数の本体の中で利用できます。
内側の関数は外側の関数本体の一部で、xの値にアクセスできます。
別の言い方をすると、x = 5は内側の関数の環境の中にあります。
技術的には、これはクロージャとして知られています。
環境はドット引数で特に重要です。この方法で渡された値は、関数本体の中の関数呼び出しにそのまま渡すことができるからです。 外側の関数は、ドット引数が何を意味するかを知る必要も、気にする必要もありません。
f_var <- function(x, ...) {
sum(x, ...)
}
x <- c(1, 2, NA, 6)
# for sum(), na.rm defaults to FALSE
f_var(x)
#> [1] NA
# pass through the na.rm value
f_var(x, na.rm = TRUE)
#> [1] 9
このテクニックは、stringrのようなTidyverseのライブラリで広く使われています。
stringrの関数の多くは、stringiやbase Rの低レベルな関数を、使いやすくラップしたものです。
str_*()関数に渡された追加の引数は、そのままこれらの低レベルな関数に渡されます。
外側の関数は内側の関数(名前付き、または後述する無名のもの)を定義し、それを戻り値として使うことができます。
返される関数には、それが定義された環境が含まれます。
times_y <- function(x) {
# anonymous function - see next section
\(y) x * y
}
f <- times_y(3)
class(f)
#> [1] "function"
f(4)
#> [1] 12
関数を定義するとき、通常はできあがった関数オブジェクトを変数にバインドします。
x <- 2
# function definition captures x from the environment
doubleit_short <- function(y) x * y
これにより、スクリプトの後半でその関数を簡単に使えますが、このようなバインドは必須ではありません。 短く、一度だけ使う関数は、その場の文脈で役立つことがあります。 名前をバインドしない場合、それは無名関数と呼ばれます。
無名関数の使用はとても一般的なので、(R v4.1.0以降)それらを定義する短縮構文があります。functionという語をバックスラッシュ\に置き換えます。
これの例は、戻り値についての前のセクションで使いました。
Rでは、ベクトルの個々の要素に代入できます。 ベクトルを関数の引数として渡し、返す前に関数本体の中で変更すると、変更されたベクトルが得られます。
しかし、元のベクトルはどうなったのでしょうか?
f <- function(vec) {
vec[1] <- 42
vec
}
vals <- c(1, 3, 4)
# f() returns a modified vector
f(vals)
#> [1] 42 3 4
# the original vector is unchanged.
vals
#> [1] 1 3 4
Rはデータサイエンスのために設計された言語です。 そのデータを集めるには、多くの時間と労力がかかり、場合によっては莫大な金額がかかることもあります。データを壊さないことが大切です!
一般的な方針は(いくつかの例外を除いて)変更時にコピーです。 オブジェクト(たとえばベクトル)が、後で問題を引き起こす可能性のある形で変更された場合、Rは変更されたコピーを返し、元のオブジェクトには手を付けません。
大きなデータ構造のコピーは計算コストが高くなることがありますが、もう一方の選択肢がデータの破損であることを考えれば、一般的にはこちらがましな方です。
同じデータを繰り返しコピーすることにつながる操作には注意してください。
(Tidyverseの作者)Hadley Wickhamの言葉を言い換えると、次のようになります。
ループはそれ自体が遅いわけではありませんが、ループの中に遅い処理をうっかり入れてしまいやすくなります。
ベクトル化や高階関数を使えば、この種のパフォーマンスを大きく損なうものから身を守るのに役立ちます。