関数について

概要

講義: 45 分
演習: 15 分
質問
  • R で関数はどのように書きますか?

目標
  • 引数を持つ関数を定義しましょう。

  • 関数から値を返しましょう。

  • 関数の中で stopifnot() を使って引数の状態を検査しましょう。

  • 関数をテストしましょう。

  • 関数の引数に既定値を指定しましょう。

  • なぜプログラムを小さな一つの目的を持つ関数に分けるべきか説明出来るようになりましょう。

分析したいデータセットが一つだけなら、ファイルを表計算ソフトで読み込み、単純な統計値をプロットした方が早いでしょう。 しかし、gapmider データは定期的に更新されるので、後から新しい情報を読み込み分析し直したくなります。 また、将来的には似たようなデータを違う場所から入手することもあるでしょう。

この講義では関数の書き方を学ぶことで、同じ操作を一つのコマンドで繰り返せるようになります。

関数とは何でしょう?

関数は連続した操作を一つに纏め、後から使うときのために保存しておきます。 関数は以下の機能を提供します。:

  • 記憶可能で実行可能な名前
  • 各操作を覚える必要性からの解放
  • 入力と出力の組み合わせを定義
  • より大規模なプログラミング環境への接続性の高さ

ほとんどのプログラミング言語における基本的なブロックの構築がそうであるように ユーザーが定義する関数はどんな単純なことであれ抽象化する「プログラミング」により成り立ちます。 関数を書いた時点であなたはコンピュータープログラマーです。

関数を定義しましょう。

functions/ ディレクトリ内に新しく functions-lesson.R と名付けた R スクリプトを作成して開きましょう。

my_sum <- function(a, b) {
  the_sum <- a + b
  return(the_sum)
}

華氏をケルビンに変換する fahr_to_kelvin() という関数を定義しましょう。

fahr_to_kelvin <- function(temp) {
  kelvin <- ((temp - 32) * (5 / 9)) + 273.15
  return(kelvin)
}

fahr_to_kelvin() を定義するには、fahr_to_kelvinfunction の出力を指定します。 引数名の一覧は括弧の中に書きます。 次に関数の本文 (body) として 走らせた時の実行内容を波括弧 ({}) の中に記述します。 本文はスペース二つでインデントしておきます。 これによりコードの操作内容を変更せずに可読性を向上させます。

関数を作ることは料理本を書くようなものだと思うといいでしょう。 まず、必要な「材料」を決めます。 今回の関数に必要な材料は「temp」一つだけです。 材料を列挙したら、材料を使って何をするかを決めます。 今回は材料に対して数学的な演算を行います。

関数を呼び出す時、引数に指定した値は関数内で用いられる変数に与えられます。 関数の中では関数を呼び出した相手に結果を送るために return 文 を用います。

ヒント

return 文が不要なことは R の変わった特徴の一つです。 R では関数の本文の最終行に記述された変数が自動的に返り値になります。 しかし、わかりやすくするために return 文を明示的に記述します。

関数を実行してみましょう。 自分の関数を呼び出す方法は他の関数を呼び出す方法と同じです。

# 水の凝固点
fahr_to_kelvin(32)
[1] 273.15
# 水の沸点
fahr_to_kelvin(212)
[1] 373.15

チャレンジ1

ケルビンを入力するとセ氏を返す kelvin_to_celsius() という関数を書いて下さい。

ヒント: ケルビンをセ氏に換算するには 273.15 を引きます。

チャレンジ1の解答

ケルビンを入力するとセ氏を返す kelvin_to_celsius() という関数を書いて下さい。

kelvin_to_celsius <- function(temp) {
 celsius <- temp - 273.15
 return(celsius)
}

関数を組み合わせましょう

関数の真髄を発揮するのは、関数を混ぜ合わせ組み合わせてより多きな塊にすることで、望み通りの効果を得る時です。

華氏をケルビンに変換する関数とケルビンをセ氏に変換する関数の2つを定義しましょう。

fahr_to_kelvin <- function(temp) {
  kelvin <- ((temp - 32) * (5 / 9)) + 273.15
  return(kelvin)
}

kelvin_to_celsius <- function(temp) {
  celsius <- temp - 273.15
  return(celsius)
}

チャレンジ2

先の2つの関数を再利用するか自分で定義した関数を利用して 華氏を直接セ氏に変換する関数を定義して下さい。

チャレンジ2の解答

先の2つの関数を再利用して華氏を直接セ氏に変換する関数を定義して下さい。

fahr_to_celsius <- function(temp) {
  temp_k <- fahr_to_kelvin(temp)
  result <- kelvin_to_celsius(temp_k)
  return(result)
}

幕間: 防衛的プログラミング

関数を書くことで R のコードを効率的に再利用したりモジュール化する方法を理解し始めたところですが、 関数は想定した用途でのみ機能するように確実に設計することが重要です。 関数の引数を検査することは防衛的プログラミングの考え方に繋がります。 防衛的プログラミングでは状況を頻繁に検査し何かおかしなことがあればエラーを返すことを推奨します。 このような検査は、プログラム実行を継続する前に現状が TRUE であることをアサート(表明・断言)するので、アサーション文と呼ばれます。 アサーション文により、エラーがどこで起きているか分かりやすくなりデバッグが容易になります。

stopifnot() を用いて状態を検査しましょう

華氏をケルビンに変換する fahr_to_kelvin() 関数について再検討してみましょう。 この関数の定義は以下の通りです。

fahr_to_kelvin <- function(temp) {
  kelvin <- ((temp - 32) * (5 / 9)) + 273.15
  return(kelvin)
}

この関数が意図した通りに機能するには、temp 引数は数値でなければなりません。 さもなくば、片方の温度の尺度からもう一方へ変換する計算過程が機能しません。 エラーを返すためには、stop 関数を使います。 例えば、temp 引数は数値ベクトルである必要があるので、 if 文により temp が数値ベクトルであるか確認し、違反していればエラーを返すようにします。 先述の関数の場合は以下のように増補します。

fahr_to_kelvin <- function(temp) {
  if (!is.numeric(temp)) {
    stop("temp must be a numeric vector.")
  }
  kelvin <- ((temp - 32) * (5 / 9)) + 273.15
  return(kelvin)
}

複数の状態や引数を検査する必要があると、全てを検査するためのコードは何行にも渡ります。 幸いなことに R は stopifnot という便利な関数を提供しています。 TRUE と評価されるべき要件を必要なだけ列挙すると、 stopifnot() は一つでも FALSE がある場合にエラーを返します。 検査項目を列挙すると、追加のドキュメント化という2つ目の目的としても機能します。

stopifnot() を用いて fahr_to_kelvin() に入力を検査するアサーション文を追加し、 防衛的プログラミングに挑戦しましょう。

temp が数値ベクトルであることをアサートしたいとします。 以下のようにしましょう。

fahr_to_kelvin <- function(temp) {
  stopifnot(is.numeric(temp))
  kelvin <- ((temp - 32) * (5 / 9)) + 273.15
  return(kelvin)
}

入力が適切であればこれでも機能します。

# 水の凝固点
fahr_to_kelvin(temp = 32)
[1] 273.15

しかし不適切な入力があるとすぐに失敗します。

# Metric is a factor instead of numeric
fahr_to_kelvin(temp = as.factor(32))
Error in fahr_to_kelvin(temp = as.factor(32)): is.numeric(temp) is not TRUE

チャレンジ3

防衛的プログラミングにより、fahr_to_celsius() 関数の temp 引数に不適切な 値が指定されたらすぐにエラーを返すよう念押しして下さい。

チャレンジ3の解答

明示的に stopifnot() を呼ぶことで先述の関数の定義を拡張しましょう。 fahr_to_celsius() は2つの他の関数から構成されているので、 ここでの検査は2つの関数の検査に追加され冗長になります。

fahr_to_celsius <- function(temp) {
  stopifnot(!is.numeric(temp))
  temp_k <- fahr_to_kelvin(temp)
  result <- kelvin_to_celsius(temp_k)
  return(result)
}

もっと関数を組み合わせましょう

ここで我々のデータセットで利用できるデータからある国の国内総生産を計算するための関数を定義します。

# データセットを受け取り、人口の列と一人あたりのGDPをかけます。
calcGDP <- function(dat) {
  gdp <- dat$pop * dat$gdpPercap
  return(gdp)
}

calcGDP() を定義するために、function の結果を calcGDP に代入します。 引数の名前の一覧は括弧に中に書きます。 次に、本文 – 関数を読んだ時に実行される命令文 – は波括弧 ({}) の中に書きます。

本文中の命令文は2つのスペースでインデントしました。 これにより関数の動作に影響を及ぼさずに可読性を向上できます。

関数を呼び出す時に、関数に渡した値は引数に指定され、 関数の本文中における変数になります。

関数の中では return() 関数を用いて結果を返します。 return() 関数は必須ではなく、R は 関数の最終行で実行されたコマンドの結果を自動的に返します。

calcGDP(head(gapminder))
[1]  6567086330  7585448670  8758855797  9648014150  9678553274 11697659231

これでは情報に乏しいです。いくつか引数を追加して、年ごとと国ごとの情報を得られるようにしましょう。

# データセットを受け取り、人口の列と一人あたりのGDPの列をかけます。
calcGDP <- function(dat, year=NULL, country=NULL) {
  if(!is.null(year)) {
    dat <- dat[dat$year %in% year, ]
  }
  if (!is.null(country)) {
    dat <- dat[dat$country %in% country,]
  }
  gdp <- dat$pop * dat$gdpPercap

  new <- cbind(dat, gdp=gdp)
  return(new)
}

もしこれらの関数を別の R スクリプトに書いているなら (グッドアイディア!)、 source() 関数を使って関数を R セッションに読み込むことができます。

source("functions/functions-lesson.R")

さて、この関数の中では色々なことが起きています。 平文にすると、この関数は year 引数を指定されている場合に、指定した値でデータを絞り込みます。 次にその結果を、country 引数が指定されている場合に指定した値で絞り込みます。 そしてこの2つの段階を経て現れたデータに対してGDPを計算します。 更にこの関数は、絞り込んだデータに新しくGDPの列を追加し、最終結果として返します。 出力が単に数値のベクトルだった時よりもはるかに情報に富んでいることがわかるでしょう。

year を指定した時に何が起きるか見てみましょう。

head(calcGDP(gapminder, year=2007))
       country year      pop continent lifeExp  gdpPercap          gdp
12 Afghanistan 2007 31889923      Asia  43.828   974.5803  31079291949
24     Albania 2007  3600523    Europe  76.423  5937.0295  21376411360
36     Algeria 2007 33333216    Africa  72.301  6223.3675 207444851958
48      Angola 2007 12420476    Africa  42.731  4797.2313  59583895818
60   Argentina 2007 40301927  Americas  75.320 12779.3796 515033625357
72   Australia 2007 20434176   Oceania  81.235 34435.3674 703658358894

また country を指定するとどうなるでしょうか。

calcGDP(gapminder, country="Australia")
     country year      pop continent lifeExp gdpPercap          gdp
61 Australia 1952  8691212   Oceania  69.120  10039.60  87256254102
62 Australia 1957  9712569   Oceania  70.330  10949.65 106349227169
63 Australia 1962 10794968   Oceania  70.930  12217.23 131884573002
64 Australia 1967 11872264   Oceania  71.100  14526.12 172457986742
65 Australia 1972 13177000   Oceania  71.930  16788.63 221223770658
66 Australia 1977 14074100   Oceania  73.490  18334.20 258037329175
67 Australia 1982 15184200   Oceania  74.740  19477.01 295742804309
68 Australia 1987 16257249   Oceania  76.320  21888.89 355853119294
69 Australia 1992 17481977   Oceania  77.560  23424.77 409511234952
70 Australia 1997 18565243   Oceania  78.830  26997.94 501223252921
71 Australia 2002 19546792   Oceania  80.370  30687.75 599847158654
72 Australia 2007 20434176   Oceania  81.235  34435.37 703658358894

あるいは両方指定してみましょう。

calcGDP(gapminder, year=2007, country="Australia")
     country year      pop continent lifeExp gdpPercap          gdp
72 Australia 2007 20434176   Oceania  81.235  34435.37 703658358894

関数の本文を順番に見ていきましょう。

calcGDP <- function(dat, year=NULL, country=NULL) {

ここで yearcountry の二つの引数を追加しました。 = 演算子を関数定義時に用いることで、両者の既定値には NULL を指定しています。 これにより、ユーザーが値を指定しない限り、これらの引数の値は NULL になることを意味します。

  if(!is.null(year)) {
    dat <- dat[dat$year %in% year, ]
  }
  if (!is.null(country)) {
    dat <- dat[dat$country %in% country,]
  }

ここでは、追加した引数それぞれについて値が NULL であるか確認し、 NULL でなければ dat に格納されたデータセットを非 NULL な引数の値を用いて絞り込み上書きします。

こうして関数を柔軟なものにすることで、後々使いやすくしました。 この関数を用いて、以下の様々な場合のGDPを計算できます。

代わりに %in% を使うことによって、yearcountry に複数の値を指定できるようになっています。

Tip: 値渡し

R の関数は関数本文中で操作するためのデータを、ほぼ必ずコピーします。 関数の中で dat を変更するということは、 dat に格納された gapminder データセットのコピーを変更するということであり、 第一引数に指定した元の変数を変更することはありません。

この挙動は “値渡し” と呼ばれ、コードの記述をより安全なものにします。 つまり、関数内での変更は常に関数内での出来事に留まります。

Tip: 関数のスコープ

スコープはもう一つの重要な概念です。 関数の本文中で作成したり変更したいかなる変数 (関数を含む!) は、 関数を実行している間だけ存在します。calcGDP() を呼んだ時に、 datgdp、そして new という変数は関数の本文中でのみ存在します。 対話的な R のセッションにおいて同名の変数が存在していたとして、 それらは関数実行時に変更されることはありません。

  gdp <- dat$pop * dat$gdpPercap
  new <- cbind(dat, gdp=gdp)
  return(new)
}

最終的に、絞り込んだデータからGDPを計算し、その結果を列に追加した新しいデータフレームを作成しました。 これは関数を呼び出した後でも返り値のGDPの値が持つ文脈がわかることを意味します。 従って、最初に試した数値のベクトルを返す方法よりもずっと良いものです。

チャレンジ4

GDP を計算する関数をテストするため、1987年の New Zealand の GDP を計算して下さい。 1952 年の New Zealand の GDP とはどう違いますか?

チャレンジ4の解答

  calcGDP(gapminder, year = c(1952, 1987), country = "New Zealand")

1987年の New Zealand の GDP: 65050008703

1952年の New Zealand の GDP: 21058193787

チャレンジ5

paste() 関数を使うと文字列を一繋ぎにできます。例えば

best_practice <- c("Write", "programs", "for", "people", "not", "computers")
paste(best_practice, collapse=" ")
[1] "Write programs for people not computers"

textwrapper という引数を持ち、textwapper で囲む fence() という関数を定義して下さい。

fence(text=best_practice, wrapper="***")

Note: paste() 関数では sep 引数を用いて文字列の間に入る文字列を指定できます。 paste0() の場合 sep の既定値は “” です。

チャレンジ5の解答

textwrapper という引数を持ち、textwapper で囲む fence() という関数を定義して下さい。

fence <- function(text, wrapper){
  text <- c(wrapper, text, wrapper)
  result <- paste(text, collapse = " ")
  return(result)
}
best_practice <- c("Write", "programs", "for", "people", "not", "computers")
fence(text=best_practice, wrapper="***")
[1] "*** Write programs for people not computers ***"

ヒント

R より複雑な演算を行う時に利用できる変わった機能があります。 ここでは発展的な概念を知っておく必要のあることは書きません。 将来的に R で関数を書くことに慣れたら、 R Language Manual や Hadley Wickham による Advanced R Programming のこのを読んで学んで下さい。

Tip: テストとドキュメント

関数をテストし、ドキュメントを整備することは重要です。 ドキュメントはあなたや他の人が関数の用途や用法を理解する助けになります。 また、関数が意図した通りに動作することを確認することは重要です。 始めるにあたって、作業手順は多くの場合以下のようなものになるでしょう。

  1. 関数を書く
  2. 関数の部分的な挙動についてドキュメント代わりにコメントする
  3. ソースファイルを読み込む
  4. コンソール上で実験し、意図した通りに動作するか確認する
  5. 適宜バグの修正を行う
  6. これらを繰り返す。

別途 .Rd ファイルに記述された正式な関数のドキュメントは、 ヘルプファイルを見た時のドキュメントになります。 roxygen2 パッケージを使うと、開発者は R の関数のすぐ側にドキュメントを書き、 適切な .Rd ファイルに加工することができます。 より複雑なプロジェクトを R で扱うようになると、このようにより正式な手法を使って ドキュメントを整備したくなるでしょう。

正式な自動化されたテストを書くには testthat パッケージを使います。

まとめ

  • function を使って R で新しい関数を定義する。

  • パラメーターを用いて関数に値を渡す。

  • stopifnot() を使って R の関数の引数を柔軟に検査する。

  • source() を使ってプログラムに関数を読み込む。