Rのこと。

記事は引っ越し作業中。2023年中までに引っ越しを完了させてブログは削除予定

Advanced R: Function

はじめに

Advanced R 2nd EditionのPaperBook版が届いたので、Rについてのおさらい備忘録。また、書籍を読んでいるにも関わらず誤った解釈や用語の誤用もあるかもしれませんので、参考にする際は自己責任でお願いします。

この記事のライセンスはAdvanced R 2nd Editionと同様で下記の通りです。

Function

ここでは関数の作成における、コードの重複を減らす方法、厳密で理論的な理解、興味深いトリックやテクニックについて説明される。

関数とは

関数は3つの部分から構成される。関数は、formals()body()が明示的に指定しないといけないが、environment()は、関数を定義した場所に基づいて暗黙的に決定される。

  • formals():関数を呼び出す方法を制御する引数のリスト。
  • body():関数内のコード。
  • environment():関数名に関連付けられた値を見つける方法を決定するデータ構造。
f <- function(x, y) {
  x + y
}

formals(f)
$x

$y


body(f)
{
    x + y
}

environment(f)
<environment: R_GlobalEnv>

プリミティブ関数

Rにはプリミティブ関数という直接C言語を呼び出す関数がいくつか準備されている。これらの関数はR-coreチームしか修正、変更できない。

sum
function (..., na.rm = FALSE)  .Primitive("sum")

`[`
.Primitive("[")

これらは、特別な型を持つ。builtinspecialである。そして、プリミティブ関数はRの関数としては、基本構成要素formals()body()environment()を持っていない例外的な扱いを受ける。

typeof(sum)
[1] "builtin"

typeof(`[`)
[1] "special"

formals(sum)
NULL

body(sum)
NULL

environment(sum)
NULL

無名関数

Rでは、基本的に名前で関数をバインディングすれば関数を作成できる。下記の例では、fで関数をバインディングしている。

f <- function(x, y) {
  x + y
}

しかし、バインディングは必須ではない。関数を名前でバインディングしない場合、それは、無名関数となる。

lapply(mtcars, function(x) length(unique(x)))
$mpg
[1] 25

[略]

$carb
[1] 6

レキシカルスコープ

名前に関連付けられた値を見つけること、つまりスコープはどうなっているのか。下記の例ではなぜ20が返されるのか、スコープ規則について、「名前のマスキング」、「関数と変数」、「フレッシュスタート」、「動的ルックアップ」の観点から説明される。

x <- 10
g01 <- function() {
  x <- 20
  x
}

g01()
[1] 20

名前のマスキングとは、レキシカルスコープの基本原則で、関数の内側で定義された名前が、関数の外側で定義された名前をマスクすること。下記の例では、関数内で定義されたxyが、関数外のxyをマスクしているので、関数内でxyバインディングしている値が返される。

x <- 10
y <- 20
g02 <- function() {
  x <- 1
  y <- 2
  c(x, y)
}

g02()
[1] 1 2

この例のように、関数内で名前が見つからない場合、関数外の値を利用する。

x <- 2
g03 <- function() {
  y <- 1
  c(x, y)
}

g03()
[1] 2 1

これは、関数の中で関数が使われている場合でも同様である。

x <- 1
g04 <- function() {
  y <- 2
  i <- function() {
    z <- 3
    c(x, y, z)
  }
  i()
}
g04()
[1] 1 2 3

下記のようなことはしないはずであるが、関数呼び出しで名前を使用すると、Rはその値を探すときに関数以外のオブジェクトを無視する。可読性が下がるので完全非推奨。

g09 <- function(x) x + 100
g10 <- function() {
  g09 <- 10
  g09(g09)
}
g10()
[1] 110

フレッシュスタートとは、関数が呼び出されるたびに、新しい環境が作成されることを意味します。つまり、各呼び出しは完全に独立している。つまり、下記の例では、aがなければa1を代入するという処理であり、1回目に関数を実行した時にaがなくても、2回目に実行したときは、先ほどのaが残っているとはならない。

g11 <- function() {
  if (!exists("a")) {
    a <- 1
  } else {
    a <- a + 1
  }
  a
}

g11()
[1] 1

g11()
[1] 1

動的ルックアップについて。レキシカルスコープはどこで値を探すかを決定するが、いつ値を探すかは決定しない。Rは、関数が実行されたときではなく、関数が評価されたときに値を探す。つまり、関数の出力は、関数の環境外のオブジェクトによって異なる可能性がある。

g12 <- function() x + 1
x <- 15
g12()
[1] 16

x <- 20
g12()
[1] 21

遅延評価

Rでは、関数の引数は遅延評価される。つまり、アクセスされた場合にのみ評価されるので、アクセスされなければ評価もされない。下記の例では、xはアクセスされないので、評価されない。

h01 <- function(x) {
  10
}

h01(stop("This is an error!"))
[1] 10

遅延評価を理解する上で、 promiseへの理解も重要である。promiseは3つの要素を持つ。

  • x + yのような式で遅延評価が発生する
  • 式が評価されるべき環境、すなわち関数が呼び出される環境である
  • 最初にpromiseにアクセスしたときに計算されキた値がキャッシュされる

2番目の要素は下記を意味する。h02(y)が評価される環境は、関数が呼び出される環境。ここではグローバル環境なので11が返る。

y <- 10
h02 <- function(x) {
  y <- 100
  x + 1
}

h02(y)
[1] 11

3番目の要素は下記を意味すらしい。これはいまいち理解できなかった。

A value, which is computed and cached the first time a promise is accessed when the expression is evaluated in the specified environment. This ensures that the promise is evaluated at most once, and is why you only see “Calculating…” printed once in the following example.

double <- function(x) { 
  message("Calculating...")
  x * 2
}

h03 <- function(x) {
  c(x, x)
}

h03(double(x))
Calculating...
[1] 40 40

デフォルト引数、欠損している引数

デフォルト引数と欠損している引数の挙動について。遅延評価の仕組みを利用すれば、デフォルト引数で下記のような設定も可能。可能ではあるが非推奨とのこと。

h04 <- function(x = 1, y = x * 2, z = a + b) {
  a <- 10
  b <- 100
  
  c(x, y, z)
}

h04()
[1]   1   2 110

デフォルト引数が評価されたのかどうかは、下記のようにすれば判断できる。

h06 <- function(x = 10) {
  list(missing(x), x)
}

str(h06())
List of 2
$ : logi TRUE
$ : num 10

str(h06(10))
List of 2
$ : logi FALSE
$ : num 10

...(dot-dot-dot)

...は非常に柔軟な引数のこと。これのおかげで、関数は追加の引数をいくつでも取ることが可能となる。例えば、下記のようなときに役立ちます。

  • 関数が引数として関数を取る場合、その関数に追加の引数を...で渡すことが可能
x <- list(c(1, 3, NA), c(4, NA, 6))
lapply(x, mean, na.rm = TRUE)
[[1]]
[1] 2

[[2]]
[1] 5
  • S3ジェネリック関数ならば、メソッドが任意の追加の引数...で渡すことが可能
 print(factor(letters), max.levels = 4)
 [1] a b c d e f g h i j k l m n o p q r s t u v w x y z
26 Levels: a b c ... z

print(factor(letters))
 [1] a b c d e f g h i j k l m n o p q r s t u v w x y z
Levels: a b c d e f g h i j k l m n o p q r s t u v w x y z

一方でデメリットも存在する。...で、どこに引数を渡すのかを明示しないと可読性が下がる。また、...で引数を渡す場合、スペルを間違えてもエラーにはならない。

sum(1, 2, NA, na_rm = TRUE)
[1] NA

エラーハンドリング

関数の実行が完了できない場合、エラーをハンドリングしておく必要がある。stop()を使えば、関数の実行を即座に終わらせることが可能。

j05 <- function() {
  stop("I'm an error")
  return(10)
}

j05()
Error in j05(): I'm an error

終了ハンドラ

on.exit()を使うことで、関数が正常に終了したか、エラーで終了したかがわかる。

j06 <- function(x) {
  cat("Hello\n")
  on.exit(cat("Goodbye!\n"), add = TRUE)
  
  if (x) {
    return(10)
  } else {
    stop("Error")
  }
}

j06(TRUE)
Hello
Goodbye!
[1] 10

j06(FALSE)
Hello
Error in j06(FALSE): Error
Goodbye!

関数形式

関数呼び出し形式には4つの種類がある。

  • 接頭(prefix):関数名が引数の前ある関数。func(a,b,c)
  • 中置(infix):関数名が引数の間にある関数。x + y
  • 置換(replacement):代入で値を置き換える関数。names(df) <- c("a", "b", "c")
  • 特別(special):[[ifforなどの上記に該当しない関数。

Rでは、すべての中置、置換、または特別形式を接頭辞形式に書き換えることができる。

x + y
`+`(x, y)

names(df) <- c("x", "y", "z")
`names<-`(df, c("x", "y", "z"))

for(i in 1:10) print(i)
`for`(i, 1:10, print(i))

また、バックティック%で囲むことで、独自の中置関数を作成することもできる。

'%nin%' <- function(x, y) !(x %in% y)

`%xor%` <- function(a, b) xor(a, b)

置換関数とは、引数を適切に修正し、特別な名前を付けるように機能する関数のこと。

`second<-` <- function(x, value) {
  x[2] <- value
  x
}

x <- 1:10
second(x) <- 5L
x
[1]  1  5  3  4  5  6  7  8  9 10