ここでは,メソッドを式として使う方法,メソッドの再帰呼び出しなど, より発展的なメソッドの使用法について説明します.
もくじ
- 例題
- プログラム
- return --- メソッドの返り値
- returnがない場合のメソッドの返り値
- 複数の返り値
- 引数のデフォルト値の指定
- メソッドの定義と起動の順序
- ローカル変数 --- メソッドの中の変数
- メソッドの再帰呼び出し
例題
コッホ(Koch)曲線は代表的なフラクタル曲線の一つです. 1本の線分を三等分して,中央の1/3の線分を底辺とする正三角形を作って底辺を取り除くと,4つの線分がつながった折れ線に変わります. ここで得られた4本の各線分に対して,さきほどと同じ変換操作をもう一度行います. その結果得られる各線分について,さらに同じ変換操作を行います. 同様の変換操作を無限に繰り返したときに得られる図形がコッホ曲線です.
本物のコッホ曲線を描くことはできません. しかし何段階かの変換操作を行うことで近似的にコッホ曲線を表す図形を得ることができます. 今回のプログラムでも,実際に描くのはコッホ曲線の近似図形とします.
プログラム
例題のプログラムをRubyで記述すると以下のようになります.
[行番号つきプログラムを別のウィンドウで開く] [行番号なしのプログラム]
このプログラム(gtr_koch.rb)は次のように実行します.
ここで指定しているパラメタは順に「近似レベル」,「始点x座標」,「始点y座標」,「曲線を延ばす方向」,「曲線のベースとなる線分の長さ」を表しています. これらのパラメタが指定されない場合には,描画時にパラメタ入力を行います.
return --- メソッドの返り値
以前に「変数とデータ入力」の資料の「オブジェクトとメソッド再び」でも説明した通り,メソッドは式としての値をもちます. その値をメソッドの返り値(return value)といいます.
メソッドの定義の中で「return 式」と記述すると,「式」の値がメソッドの返り値になります. メソッド呼び出しが代入式の右辺になっていれば,この返り値が代入式の左辺の変数に対応づけられます.
次の例でlen_1は右辺のnext_len(len)を評価した結果の値,つまりlen/3.0となります.
49 # 次のレベル(level-1)でのコッホ曲線の長さ
50 len_1 = next_len(len)
:
:
60 # 長さlengthの曲線の次のレベルでの長さ
61 def next_len(length)
62 # ここではメソッドの値を返す「return」は省略可能
63 # つまりlength/3.0と書くだけでもよい.
64 # メソッドの最後の式の値がメソッドの値になる.
65 return length/3.0
66 end
なお返り値の式を省略することもできます(「return」とだけ書く). この場合はnilが返り値となります. メソッドの返り値が使われない場合には,このようにreturnだけの式を書いても構いません.
40 def koch(level,len,theta=0)
41 turn(theta) # まずtheta回転する
42
43 # 曲線の長さが十分短いか,level==0ならば単に線を描く
44 if len < 2 || level == 0
45 forward(len)
46 return # メソッド実行終了
47 end
returnがない場合のメソッドの返り値
メソッドにreturnがない場合は,メソッドの最後まで処理を行って最後に評価した式の値が返り値になります. したがって,最後の式をそのまま返せばよいような場合は,returnを省略しても構わないことになります.
60 # 長さlengthの曲線の次のレベルでの長さ
61 def next_len(length)
62 # ここではメソッドの値を返す「return」は省略可能
63 # つまりlength/3.0と書くだけでもよい.
64 # メソッドの最後の式の値がメソッド
65 length/3.0 # こう書いてもlength/3.0が返り値となる
66 end
ただし,条件分岐があるときなど,returnを省略できないときもあります.
複数の返り値
一つのメソッドで複数の式の値を返すこともできます. その場合には,returnの後に返したい式を「,」で区切って並べます. またそのようなメソッドから返される複数の値を代入で受け取るには,代入の左辺に返り値の数だけ変数を「,」で区切って並べます. このような仕組みを多重代入といいます.
例えば次のようなメソッドが書けます.
# x,yについて「y/x」と「y%x」を返すメソッド def div_mod(y,x) return y/x,y%x end # qに17/3(17を3で割った商),rに17%3(17を3で割った余り)が代入される q,r = div_mod(17,3) # q == 5, r == 2
例題でも複数の値を返すメソッドを使っています.
25 level,x0,y0,dir,len = get_parameters()
:
:
68 # 描画するコッホ曲線のパラメタ(level,x0,y0,dir,len)を取得する
69 def get_parameters() # 引数なしのメソッドの定義→()は省略可能
70 level = ask('レベル?').to_i
71 x0,y0 = ask('始点(x,y)?').split(',').collect { |v| v.to_i }
72 dir = ask('方角?').to_i
73 len = ask('長さ?').to_i
74 # 次の行は[level,x0,y0,dir,len]と書いても同じ
75 return level,x0,y0,dir,len
76 end
77 end
引数のデフォルト値の指定
Rubyでは,メソッド定義において,引数にデフォルト値(default value;既定値)を指定することができます. メソッドの仮引数のデフォルト値とは,メソッド呼び出しで実引数が与えられなかったときに,その仮引数に対応づけられる値のことです. 引数のデフォルト値を指定するには,メソッド定義の先頭行で 「仮引数=デフォルト値」 のように記述します.
# aのn乗を返す.ただし引数が1個の場合には2乗を返す
def pow_n_or_squ(x,n=2) # 第2引数nが与えられない場合は,n=2とする.
return a**n
end
p pow_n_or_squ(2,3) # ==> 8
p pow_n_or_squ(2.5,-3) # ==> 0.064
p pow_n_or_squ(5) # ==> 25
p pow_n_or_squ(Math::PI) # ==> 9.869604401 (π2)
例題のプログラムのメソッドkochでもデフォルト値を使っています.
38 # theta回転した方向に長さlenのレベル=levelのコッホ曲線を描く
39 # thetaのデフォルト値を0とする
40 def koch(level,len,theta=0)
メソッドの定義と起動の順序
変数を定義する前に,その変数の値を参照できないように,メソッドを起動するときには,そのメソッドが定義済でなければなりません.
a = foo(2) # ==> エラー (fooは定義されていない) def foo(x) y = x*x z = y+2*x+1 return z end
ローカル変数 --- メソッドの中の変数
メソッドの中で現れる変数は,メソッドの内部のみで有効で外部では利用できません.
def foo(x) y = x*x # z = y+2*x+1 # x,y,zはメソッドfooの内部のみで有効 return z # end a=foo(2) a = a + x # ==> エラー (xは定義されていない)
変数の有効範囲をスコープ(scope)といいます. メソッドの中の変数のスコープは,メソッドの中でその変数が代入式の左辺として出現した後になります (その代入式が変数の定義となります). このようにスコープが制限されている変数をローカル変数(local variable)といいます. なお,じつはメソッドの外側で定義される変数もローカル変数です. すなわちそのような変数もいつでもどこでも有効なわけではありません.
メソッドの再帰呼び出し
メソッドの中で,そのメソッド自身を呼び出す(使う)ことができます. そのようにメソッドが自分自身から呼び出されることを再帰呼び出しといいます. ある問題がそれと同様な部分問題に分解できる場合, 再帰呼び出しを用いれば,問題の構造を素直に記述することで処理を実現できます.
ここで再帰呼び出しによって定義されるメソッドの例を二つ挙げます. まず一つめは階乗の計算です. 非負整数nについてnの階乗とは「1×2×...×n」です(ただし0の階乗は1と定義する). 階乗(fact)は非負整数(n)に対して,次のように定義できます.
fact(n) = n × fact(n-1) (n >= 1) fact(0) = 1
ここで,fact(n)はfact(n-1)を使って定義されていることに注目して下さい. つまりサイズnの問題がサイズ(n-1)の問題を解くこと(とそれにnを乗じること)に帰着されているわけです. またn=0のときは,直接に解が与えられています. したがって,nが非負整数であれば,fact(n)の計算はfact(n-1),fact(n-2),...を計算する問題に分解されていって,最後には必ずfact(0)を計算する場合に帰着されます. fact(0)の値は直接与えられていますので,fact(n)が計算できることが分かります. この階乗fact(n)をRubyの再帰呼び出しのメソッドとして定義すると次のようになります.
def fact(n) if n == 0 1 else n * fact(n-1) end end
再帰呼び出しによって定義される関数の二つめの例として, 次の二項係数(nCk)を挙げます. 二項係数は非負整数(n,k)について次のように定義できます.
comb(n,k) = comb(n-1,k-1) + comb(n-1,k) (n > 0, 0 < k < n) comb(n,k) = 1 (k = 0 or k = n)
階乗の場合と同様に, サイズnの問題がサイズ(n-1)の問題二つに分解されています. comb(n,k)を計算する再帰呼出しメソッドは次のようになります.
def comb(n,k)
if k == 0 or k == n
1
else
comb(n-1,k-1) + comb(n-1,k)
end
end
[参考] 再帰呼び出しでの問題
再帰呼び出しメソッドは問題の構造を素直に記述することで実現できます. これはプログラムを作成する上では大きなメリットです. しかし一方で再帰呼び出しには以下に述べるような問題があります.
上で取り挙げた二項係数を計算する関数を再帰的に定義するのはよい方法ではありません. その理由を説明するためにn=5,k=2の場合の再帰呼び出しの関係を図に表してみます.
comb(5,2)─┬─comb(4,2)─┬─comb(3,2)─┬─comb(2,2) │ │ │ │ │ └─comb(2,1)─┬─comb(1,1) │ │ │ │ │ └─comb(1,0) │ │ │ └─comb(3,1)─┬─comb(2,1)─┬─comb(1,1) │ │ │ │ │ └─comb(1,0) │ └─comb(2,0) │ └─comb(4,1)─┬─comb(3,1)─┬─comb(2,1)─┬─comb(1,1) │ │ │ └─comb(3,0) │ └─comb(1,0) └─comb(2,0)
同じ計算が何度も行われていることが分かるかと思います. 計算するには時間(とデータ記憶領域)が必要になります. 同じ計算を何度も行うことは非効率です. nが大きくなればなるほど,再帰呼び出しで同じ計算を何度も行うことの影響は,より重大なものとなります.
さて階乗を計算するプログラムでは上で述べたような問題はありません. 再帰呼び出しにおいて「枝分かれ」がないためです.
fact(n)─fact(n-1)─………─fact(0)
それでは階乗の計算の場合,再帰呼出しを使っても何も問題はないのでしょうか.
一般にメソッドを実行するときには,メソッドでの一連の処理を適切に制御するために関連するデータをシステム側で一時的に保持しておくことが必要となります(メソッドに渡す引数の値のデータ,「メソッドが終了した後に処理を続行する」ためのデータなど). そのようなデータはメソッドを開始してから終了するまで保持されます.メソッドの処理が終わると制御データは捨てられます.
ここで,あるメソッドから別のメソッドを呼び出したときのことを考えてみます. たとえばf(x,y)というメソッドがあって,そのメソッドf(x,y)は実行中に別のメソッドg(y)を呼び出すとします. この場合,f(x,y)の処理においてg(y)を実行する時点ではf(x,y)の実行は終了していないため, f(x,y)の制御データを保持したまま,メソッドg(y)を制御するデータを上乗せして保持することになります.
def f(x,y) # f(x,y)を呼び出すと制御データが確保される z = g(y) # f(x,y)の制御データに加えて,g(y)の制御データが確保される w = x*x # この時点でg(y)の制御データは解放されている return w*z # メソッドの実行が終了すればf(x,y)の制御データは解放される end
再帰呼び出しでは,メソッドの呼び出しの最中に(つまりメソッドの処理が終わる前に)そのメソッドが再び呼び出されるときに, 最初の呼び出しの制御データを保持したまま,次のメソッド呼び出しを制御するデータを上乗せして保持することになります. つまり再帰呼び出しの場合,呼び出しのたびに「メソッド実行を制御する」データが次々と蓄積されていくことになります.
def fact(n) # fact(n)を呼び出すと制御データが確保される. if n == 0 1 else # fact(n)の制御データを確保したまま,fact(n-1)を呼び出す. # (再帰呼出しでfact(n-1)の制御データを追加で確保することになる) # fact(n-1)の計算が終了するとfact(n-1)の制御データは解放される. # その後で乗算が行われる. # メソッドが終了すれば制御データは解放される. n * fact(n-1) end end
コンピュータ内のデータを記憶するスペースには限界があります. そのために再帰呼出しはいくらでも実行できるわけではありません. 呼び出しを重ねていくと,制御データが蓄積されつづけていって, 呼び出し回数が限界を越えると,データを蓄積するためのスペースを使いきってしまって,それ以上データを蓄積できなくなって,エラーが発生してしまいます(どの程度まで処理できるかはシステム次第です).
次に例(factorial_rec.rb)を示します.
def fact(n) puts "fact(#{n})" if n == 0 1 else n*fact(n-1) end end if ARGV.size < 1 STDERR.puts "#{File.basename($0)} n" exit(1) end n = ARGV.shift.to_i y = fact(n) puts "#{n}! = #{y}"
$ ruby factorial_rec.rb 10 fact(10) fact(9) fact(8) fact(7) fact(6) fact(5) fact(4) fact(3) fact(2) fact(1) fact(0) 10! = 3628800 # OK $ ruby factorial_rec.rb 10000 fact(10000) fact(9999) fact(9998) : : : fact(1816) fact(1815) factorial_rec.rb:6: stack level too deep # ERROR!!
二項係数や階乗は,再帰的に処理を行うよりも,反復的(iterative)に処理を行った方が効率的になります. 反復的処理は,変数を使って計算の途中結果を記録しながら繰り返し処理を使うことで実現できます.
たとえば階乗の計算は次のように反復処理で実現できます.
def fact_iter(n) y = 1 2.upto(n) do |i| # n < 2なら1回も実行しない y = y * i end y end