関数は、以前に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 とまったく同じ代入です。
PythonやJuliaのような他のスクリプト言語とは異なり、Rは位置引数とキーワード引数を明確には区別しません。
関数呼び出しでは、値を位置で渡すことも、名前で渡すこともできます。名前で渡す方法は、引数が多くて順番を覚えにくい複雑な関数で役立ちます。
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の言葉を言い換えると、次のようになります。
ループそれ自体が遅いわけではありません。ただ、ループの中に遅い処理をうっかり入れてしまいやすいのです。
ベクトル化や高階関数を使うと、この種のパフォーマンスキラーから身を守るのに役立ちます。
会社では、Webページを作るために地図データを扱ってきました。 いくつか話し合った結果、一部の計算を動的に行うためにRを使い始めることになりました。
点はベクトルのまま直接扱うようにしてください。xとyの座標に分解する必要はありません。
任意の次元数の点を受け取り、あらかじめ定義されたsでスケーリングするscale(point, s)関数を実装してください。
# A 2D example
s <- c(0.5, 0.8)
point <- c(10, 5)
scale(point, s)
#> [1] 5 4
translate(point, ...)関数を実装してください。この関数は、...に渡した値だけ各座標を移動させた新しい点を返します。
この関数は、任意の次元の点を扱える必要があり、ドット引数の数はその次元と一致していなければなりません。
point2d <- c(2, 3)
# supply dx, dy translations
translate(point2d, 0.5, 0.6)
#> [1] 2.5 3.6
point3d <- c(2, 3, 4)
# supply dx, dy, dz translations
translate(point3d, 0.5, 0.6, 0.7)
#> [1] 2.5 3.6 4.7
チームメイトの中にはRの経験が浅い人もいるので、{x, y}の座標ペアに対して再利用できる変換をクロージャで作ることにします。
transform2d(dx, dy, s)関数を実装してください。この関数は、クロージャを使って点の2D平行移動とスケーリングを繰り返し行える関数を返します。
スケーリングの引数sは省略可能にし、デフォルト値は1とします。
返される関数は2Dの点を受け取り、次の処理を行います。
dxとdyだけ平行移動するsでスケーリングする# scale all coordinates by the same amount (2) in this example
# your code needs to be able to handle a vector `s`
tf2d <- transform2d(0.5, 0.6, 2)
class(tf2d)
#> [1] "function"
tf2d(c(2, 3))
#> [1] 5.0 7.2
# Accept default for `s`
tf2d <- transform2d(0.5, 0.6)
tf2d(c(2, 3))
#> 2.5 3.6
操作の順序は重要です。平行移動してからスケーリングすると正しい結果になりますが、スケーリングしてから平行移動するとそうはなりません。 専門的に言えば、これらの操作は_可換ではありません_。
地図データには高さが含まれることもあり、画面上で陰影や等高線を付けるために使われます。
transform3d(dx, dy, dz, s)関数を実装してください。この関数は、別の関数を返します。
スケーリングの引数sは省略可能にし、デフォルト値は1とします。
返される関数は3Dの点を受け取り、あらかじめ定義された値だけ平行移動してから、sでスケーリングします。
# using default `s`
tf3d <- transform3d(0.5, 0.6, 0.7)
point <- c(2, 3, 4)
tf3d(point)
#> [1] 2.5 3.6 4.7
# using vector `s`
tf3d <- transform3d(0.5, 0.6, 0.7, c(0.5, 0.6, 0.7))
tf3d(point)
#> [1] 1.25 2.16 3.29