Rには豊富な関数が用意されていますが、それでも全ての要求に答えてくれるわけではありません。例えば測定機器からの出力を何らかの計算式にしたがって変換したいというような場面はしばしばあると思いますが、そんな状況のための関数がRに入っているはずもありません(いや、まあモノによっちゃ入っているかもしれないのが恐ろしいところですが)。

そんなわけで自前の関数が作りたいところです。

Rでは関数の作成がかなり簡単にできます。すでに関数の使い方を知っているなら特別覚えることもないくらいです。

自前の関数が作れると、例えば教科書に載っている数式がどんなグラフを描き出すのか?といったことも簡単に調べられるようになります(Rでは、繰り返し処理を含まない関数はplot関数やcurve関数でグラフ化が可能です)。

目次

関数を作る関数

Rでは関数も関数によって作成できます。その名もfunction関数。そしてfunction関数により作成された関数はベクトルなどと同様にオブジェクトへ代入できます。というか代入しないと使えません。

使い方はこんな感じです。
関数名 <- function(関数内で使う引数) 関数本体
とりあえず何か関数を定義してみましょう。

まずはx,yという2つの引数を与えるとその合計を返す関数を作ってみましょう。
f <- function(x, y) x+y
これで関数ができました。使い方は他の関数と同様です。
> f(2, 10)
[1] 12
基本的にはこれで説明終わりなんですが、ちょこちょこ補足を加えていきましょう。

複数行の処理と返り値

まず関数本体が複数行にわたる場合です。これはfor文などと同様に、{}で括ってしまえばOkです。
関数 <- function(引数){
   処理1
   処理2
    :
    :
   処理最後
   }
そしてこの場合、最後の処理の結果が返ってきます。この返ってくるモノのことを「返り値」と呼びます。例えば次のような関数を定義したとしましょう。
f <- function(x){
   x
   x+1
   x+2
   x+3
   }
この場合、引数として与えた数値に3を加えた値が返り値となり、出力されます。
> f(3)
[1] 6
また、返り値が代入操作などの場合、出力は行われません。
f <- function(x){
   z <- x
   }
このとき、この関数は何もできません。一見するとzというオブジェクトを作って、その中にxに与えられた数値を代入するという操作をしそうなんですが…
> f(10)
> z
エラー:  オブジェクト "z" は存在しません 
関数の中で生成され、使われるオブジェクトは「ローカル変数」と呼ばれ、関数の外で生成されたオブジェクト(これを「グローバル変数」と呼びます)には何の影響も与えません。ですから、関数を定義するときに「どこかで作ったオブジェクトと名前がかぶってしまうかもしれない><」とか不安に思う必要はありません。

また、返り値はreturn関数を使うことで明示的に指定することもできます。関数中でreturn関数が実行された場合、その時点で関数はストップします。
f <- function(x){
   x <- x+1
   return(x) #ここで終了!
   x <- x+2
   x <- x+3
   }
> f(10)
 [1] 11
この関数の場合は後半の2行は絶対に実行されないわけです。

結局返り値はどうやって指定すればいいのさ?という話ですが、関数があんまり複雑でないのなら次のようにするのがオススメです。
f <- function(x){
   x <- x+1
   x <- x+2
   x <- x+3
   x
   }
「外」でやるのと同じです。中身を見たいオブジェクトの名前を最後に呼んでやると。オブジェクトじゃなくて数式とかでもOKです(代入操作では駄目ですが)。
f <- function(x){
   x + x^2 + x^3
  }
ただ、多少複雑な関数になってくると(例えばif文やfor文を内部に含むような場合)return関数で強制的に終了させたくなる場合もありますから、returnを使うとその場で関数を終わらせることができるということも覚えておいてください。また、何が返り値なのかをはっきりさせたい時にも有効です(ただ複数の値を返そうとすると怒られます)。

関数の例

フィボナッチ数の計算

ここでは関数の例としてフィボナッチ数を挙げます。フィボナッチ数とは隣り合う2数を足し合わせたものが次の数値になる、という性質を持った数列です。つまり、
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89...
といった数列です。性質を理解するのは簡単だと思いますが、問題はこの数列の一般項を求めよ、というものです。一般項というのは数列のn番目の値という意味です。これを例えばFという文字と添え字を使って表現することにしましょう。例えばF_3ならば数列の3番目の値を示すので、F_3=2です(数列は0から数えます。つまりF_0=0。)。
これを数式で表現すると次のようになります。


0番目の値と1番目の値は最初に0と1であると定義するわけです。そして、F_2以降のF_nについては1つ前の値F_{n-1}と2つ前の値F_{n-2}を足し合わせることで計算します。

この一見簡単そうな問題が、いくつかの重要なプログラミング技術を学ぶ機会を与えてくれます。できればここからの説明はよく理解できるまで何度も読み直し、何度も実際に実行してみてください。

for文を使った解法

それでは実際に関数を作ってみましょう。上の定義を見ると、F_0F_1からスタートして、頑張って足し算を繰り返せば任意のF_nまでたどり着けそうな気がします。というわけで足し算を頑張りましょう。まずはfor文を使って頑張ります。関数化するのはひとまず置いておいて、とりあえずフィボナッチ数を計算するプログラムを書きましょう。
 n <- 10         #求めたいフィボナッチ数
 Fn2 <- 0        #n-2項の値(マイナス記号"-"はオブジェクト名に使えない)はじめなのでF0の値。
 Fn1 <- 1        #n-1項の値。はじめなのでF1の値。
 for(i in 2:n){ #F0、F1は分かっているので、F2の値の計算からスタート
    Fn <- Fn2 + Fn1
    Fn2 <- Fn1   #2つ前の値を1つ前の値に置き換え
    Fn1 <- Fn    #1つ前の値を今計算した値に置き換え(次の項は新しいFn2,Fn1で計算される)
    }
 Fn              #Fnを表示
実行してみると、この場合は55という値が返ってきます。最初に示したように、フィボナッチ数の10番目は55ですから、確かにこのプログラムでフィボナッチ数が計算できるようです。というわけで関数化しましょう。関数化といっても今のプログラムのどこかを適当に引数として、全体を中括弧で括るだけです。この場合は「求めたいフィボナッチ数」を引数にするのが適当でしょう。
my.fib.1 <- function(n){
 Fn2 <- 0        #n-2項の値(マイナス記号"-"はオブジェクト名に使えない)はじめなのでF0の値。
 Fn1 <- 1        #n-1項の値。はじめなのでF1の値。
 for(i in 2:n){ #F0、F1は分かっているので、F2の値の計算からスタート
    Fn <- Fn2 + Fn1
    Fn2 <- Fn1   #2つ前の値を1つ前の値に置き換え
    Fn1 <- Fn    #1つ前の値を今計算した値に置き換え(次の項は新しいFn2,Fn1で計算される)
    }
 Fn              #Fnを表示
 }
これで関数完成です。といいたいところですが、これではnに0と1を与えた場合に正しい値が帰ってきません。nに0を与えた場合は0を、nに1を与えた場合は1を返して強制終了するようにしましょう。強制的にストップさせるのはreturnを使うのでした。
my.fib.1 <- function(n){
 if(n==0) return(0)  #0を与えられたときは0を返して終了  ← 追加
 if(n==1) return(1)  #1を与えられたときは1を返して終了  ← 追加
 Fn2 <- 0        #n-2項の値(マイナス記号"-"はオブジェクト名に使えない)はじめなのでF0の値。
 Fn1 <- 1        #n-1項の値。はじめなのでF1の値。
 for(i in 2:n){ #F0、F1は分かっているので、F2の値の計算からスタート
    Fn <- Fn2 + Fn1
    Fn2 <- Fn1   #2つ前の値を1つ前の値に置き換え
    Fn1 <- Fn    #1つ前の値を今計算した値に置き換え(次の項は新しいFn2,Fn1で計算される)
    }
 Fn              #Fnを表示
 }
2行追加しました。ここではif文を使っています。if文は次のような構造になっています。
if(条件式) 式
括弧の中の条件式?が真のとき(TRUEである、もしくは0以外の値をとるとき)にそれに続く式が実行され、括弧の中の条件式が偽のとき(FALSEである、もしくは0のとき)には何も実行されません。

では実際にこの関数を実行してみましょう。
> my.fib.1(0)
[1] 0
> my.fib.1(1)
[1] 1
> my.fib.1(2)
[1] 1
> my.fib.1(3)
[1] 2
> my.fib.1(4)
[1] 3
> my.fib.1(5)
[1] 5
> my.fib.1(6)
[1] 8
> my.fib.1(7)
[1] 13
> my.fib.1(8)
[1] 21
> my.fib.1(9)
[1] 34
> my.fib.1(10)
[1] 55
> my.fib.1(20)
[1] 6765
> my.fib.1(30)
[1] 832040
> my.fib.1(40)
[1] 102334155
> my.fib.1(50)
[1] 12586269025
> my.fib.1(100)
[1] 3.542248e+20 
どうやら上手く計算できているようです。というわけでfor文を使った場合の解法は終了です。ポイントはif文で特定の引数のときの返り値を固定してしまったところです。

再帰呼び出しを使った解法

再帰呼び出しと呼ばれる方法があります。これは、今定義している関数の中で、まさに今定義しているその関数を呼び出すという一見特殊な方法です。何を言っているか分からないかもしれませんが、百聞は一見にしかず、です。関数の例を見てみましょう。
my.fib.2 <- function(n){
   if(n==0) return(0)     #0のときは0
   if(n==1) return(1)     #1のときは1
   my.fib.2(n-1) + my.fib.2(n-2)  #それ以外のときは前2つの値を足す
   }
0を与えられた場合と1を与えられた場合にそれぞれ定義されている値を返すのは、for文を使った場合と同じです。問題は3行目です。my.fib.2という関数の中で、my.fib.2という関数を呼び出しています。当然同じ形の関数を呼び出しているわけですから、呼び出された関数の中でもまたmy.fib.2を呼び出しているわけです。これではいつまでたっても関数を呼び出し続けてしまうような気がしますが、1行目と2行目に注目してください。nが0のときと1のときはそこで処理が終了しますから、それ以上は関数を呼び出しません。つまり、my.fib.2の関数が有無を言わさず値を返すn=0のときとn=1のときまで遡ったら、そこから次々にmy.fib.2関数へ引数が渡されていくわけです。結局やっていることは足し算の繰り返しです。

また、式の形にも注目してください。最初に説明したフィボナッチ数の式、
に良く似ている、というか全く同じです。この数式中でもF_nを計算するのにF_{n-1}F_{n-2}といった「自分自身」を呼び出しています。このような形の式を漸化式と呼びます。再帰呼び出しは漸化式をそのままプログラムの世界に持ち込んだものだと見ることができます。

再帰呼び出しによる関数は仕組みさえ理解してしまえば非常に見通しがよく、漸化式をほとんどそのまま打ち込めるので使い勝手がいいように思えます。しかしながら、再帰処理には重大な問題があります。計算測度が極めて遅いのです。

さきほどのmy.fib.1というfor文を使った関数では、桁数が足りなくてInf(無限大)を返してくるくらいの値(およそ1000ですが)を与えたとしても答えは即座に返ってきます。しかし、今再帰呼び出しを使って定義したmy.fib.2という関数は、F_{30}の値を計算するのに10秒かかりました。環境によってはもっとかかると思います。そしてF_{100}は数分待っても計算が終わりませんでした。

このように再帰呼び出しはすこし呼び出しの階層が深くなると驚くほど重くなる傾向がありますから、使用は浅い再帰の場合に限る、もしくは後で別の方法へ書き換えることを前提として使用するようにしましょう。

一般項を求める式を用いた解法

実はフィボナッチ数というのは一般項を求める式を導くことが出来ます。つまり、F_{n-1}F_{n-2}などといった「自分自身」を使うことなく、F_nの中のnという値だけでF_nの値が分かるような数式を作ることができるのです。導出は結構面倒なのでここではやりませんが、一般項は次の式で求めることができます。


ルートとかが入っていて、本当にこんなのでn番目のフィボナッチ数が分かるのかと疑いたくなる式ですが、まあとにかくこれで出てくるのですからここは受け入れてください。

それで、これをそのまま関数にしてやります。
my.fib.3 <- function(n){
   1 / sqrt(5) * ( ((1+sqrt(5))/2)^n - ((1-sqrt(5))/2)^n ) 
   }
これでフィボナッチ数を求める関数が完成です!


で、この方法のポイントは何かというと、for文も再帰繰り返しも使ってないという点です。先ほどまでに定義したmy.fib.1とmy.fib.2はいずれも繰り返し処理を含んでいますから、引数としては自然数一つのみしか与えることができませんでした。my.fib.3は整数を引数に取ることができ(つまり負のフィボナッチ数も分かるのです。負のフィボナッチ数は正と負の数が交互に出現し、ちゃんと定義どおり前の2数の和になっています。)、さらに引数としていくつもの数値をまとめたベクトルを与えることができます。

また、グラフを直接書けるのもこの方法の利点です。
plot(my.fib.3, -10, 10, type="p")
my.fib.1、my.fib.2ではそれぞれ計算結果を何かオブジェクトに収納しておいて、それからそのオブジェクトについてプロットする、という形式を採らないといけません。

ですから、最も使い勝手がいいのはfor文も再帰も使わないこの方法です。

が、最初に言ったように一般項を求めるのは面倒です。それにもしかしたら見つからないかもしれません。

まとめ

というわけでまとめです。
  • 繰り返し処理は基本的に重くなる。
  • 繰り返しなし < for文による繰り返し <<< 再帰繰り返し の順に重い。
  • できれば繰り返し処理を含まないように気をつける。
また、今回の場合はfor文を使っても比較的処理が軽く感じたと思います。しかし、繰り返し回数が10^4を超えるようなオーダになってきたとき、あるいはfor文の中でfor文を回すような処理をするときには処理の重さが問題となってくると思います。


タグ:

+ タグ編集
  • タグ:

このサイトはreCAPTCHAによって保護されており、Googleの プライバシーポリシー利用規約 が適用されます。

最終更新:2008年08月22日 19:11