Last-modified:2018/07/26 16:04:34.
前回はバグが混入しにくいプログラミングについて説明し、 その中でメソッドの作成について触れた。 プログラミングにおいては、決まった演算や処理を何度も利用することがある。 そのような処理を使う場所ごとに同じコードをコピー&ペーストすべきではない。 そのコードに修正が必要になった場合に、 同じ修正を何箇所にも適用しなければならなくなるからだ。 このような再利用性の高い処理は「メソッド」としてまとめておき、 使用する際にはそのメソッドを呼ぶようにする。
今回はメソッドの利用についての理解を深める。 まず Ruby に用意されている幾つかのメソッドを紹介する。 そのあとで自分でメソッドを定義する方法を学んでいこう。
まず、10の階乗( factorial )を計算するコードを書いてみよう。 今の諸君なら 1分以内に書ける筈だ。 本文書の末尾に factorial1.rb の模範コードを載せておくが、 まずは自分で書いてみること。 もし 3分以上かかるようならこの場で書くことは諦め、 今は模範コードを見て、帰ったあとにこれまでの内容をキチンと復習すること。 でなければ来週の試験がとても不安だ。
高校の数学で組み合わせについて習った筈だ。 たとえば セ・リーグ 6 球団の対戦の組み合わせは、 6 個の中から 2 個を選ぶ組み合わせであり、 以下のように求められる。 \[\begin{aligned} _6 \mathrm{C} _2 &=& \frac{6!}{2! 4!} = \frac{ 6 \times 5 \times 4 \times 3 \times 2 \times 1 } { ( 2 \times 1) \times ( 4 \times 3 \times 2 \times 1) } = 15\end{aligned}\]
では、この \(_6 \mathrm{C} _2\) を求めるプログラムを書いてみよう。 愚直には以下のようにも書ける。
# combination1.rb
result = ( 6 * 5 * 4 * 3 * 2 * 1) / ( (2 * 1) * (4 * 3 * 2 * 1) )
print result, "\n" #=> 15
しかし、これだとコードの再利用がしにくい。 たとえば 100C23 を求めたいときに、 「100 * 99 * ...
」などとポチポチ打っていくのは実に気が滅入る作業だ。 534556C234 といった大きな組み合わせを求めることは この方法では無理がある。 まず、 §[sec20110701a] でやったようにループを使う 1 という方法でその問題はかなり改善される。
# combination2.rb
# 分子
numerator = 1
for i in 1..6
numerator *= i
end
# 分母その1
denominator1 = 1
for i in 1..2
denominator1 *= i
end
# 分母その2
denominator2 = 1
for i in 1..4
denominator2 *= i
end
print numerator / (denominator1 * denominator2), "\n" #=> 15
上記の combination2.rb を見ると、 ソースコード全体で ほとんど同じ構造をしている部分が存在することに気付くだろう。 コーディングにおいては コピー&ペーストして一部修正という行動を取った人もいるだろう。 2 階乗は頻繁に出てくる処理なので、 もっと簡潔な書式にまとめてしまえるのならそうした方が今後も便利だろう。
そこで、階乗を求める部分をメソッドにまとめてしまおう。 前回も説明したが、「メソッド(method)」は「関数(function)」とも呼ばれる。 前回学んだメソッドの利用方法は単純に手続きをまとめたものだったが、 入力に応じた値を返す機能が関数の大きな役割だ。 3 今のところ「関数」とは、数学的な意味の関数だと思っておけば良い。 すなわち \(y = f(x)\) の \(f(x)\) のように、\(x\) を与えると一つの値を返すものだ (Fig. 1)。 ここでは「\(x\) を与えると\(x\) の階乗を返す関数」を作ることになる。 Sec. 1 で作った階乗計算プログラム factorial1.rb を改良した factorial2.rb を示そう。
# factorial2.rb
# 引数 x の階乗を返す。
def factorial( x )
result = 1
for i in 1..x
result *= i
end
return result
end
print factorial( 10 ), "\n" #=> 3628800
def
が「ここから関数の定義が始まる」というキーワードで、 ここから対応する end
までが関数定義である。 なお、def
は define の略。
def factorial( x )
の factorial
が関数の名前で これはユーザが任意に付けられる。
def factorial( x )
の ( x )
は、 関数 factorial
が実行されるときに与えられるパラメータで、 このパラメータによって関数の動作を変えることができる。 今回の関数 factorial
では、任意の \(x\) について階乗を求めるための、 この \(x\) が渡されている。 プログラミングの世界では、 この「与えられるパラメータ」のことを「引数(ひきすう, argument)」と呼ぶ。
return
は、「値を関数を呼び出した所に返す」という命令。 ここでは変数 result
の中身が返されている。 こうやって返される値を「返り値(かえりち, return value)」と呼ぶ
def
の直前の行に関数の概要を示したコメントを入れてある。 なくてもプログラムは動くが、あった方が絶対に良い。
# combination3.rb
# 引数 x の階乗を返す。
def factorial( x )
result = 1
for i in 1..x
result *= i
end
return result
end
print factorial(6) / (factorial(2) * factorial(4)), "\n" #=> 15
関数 factorial
を使ったことでかなりすっきりした。
さあ、あと一息だ。 「組み合わせを求める」作業も行う頻度が高い作業なので、 これを combination
という関数にまとめよう。
# combination4.rb
# 引数 x の階乗を返す。
def factorial( x )
result = 1
for i in 1..x
result *= i
end
return result
end
# 全体 x 個から y 個を抽出する時の組み合わせの数を返す。
def combination( x, y )
return factorial( x ) / ( factorial( y ) * factorial( x - y ) )
end
print combination( 6, 2 ), "\n" #=> 15
関数の引数は複数個でも OK。
他で定義した関数(ここでは factorial
)もそのまま使える。
これで、 最後の行の 6 や 2 を修正するだけで、 $ {100} {23}$ といった大きな数でも簡単に求められるようになる。
\(\cos x\) を求めたいときはどうしたら良いだろうか? 一つの方法は、関数の近似式を使って自分で作ってしまうことである。 また、別の方法として誰かが作った関数を利用することもできる。 本節ではこれらの方法について述べる。
\(\cos x\) のマクローリン展開による近似式は以下である。
\[\begin{aligned} \cos x &=& 1 - \frac{1}{2!} x^2 + \frac{1}{4!} x^4 - - \frac{1}{6!} x^6 + \cdots (-1)^n \frac{1}{(2n)!} x^{2n} + \cdots \\ &=& \sum_{n} (-1)^n \frac{1}{(2n)!} x^{2n}\end{aligned}\]
この公式で適当な \(n\) (たとえば10) までの近似式を使って関数を作ることができる。
# cos1.rb
# 引数 x の階乗を返す。
def factorial( x )
result = 1
for i in 1..x
result *= i
end
return result
end
# 余弦関数。ただし、x が -π <= x < π の範囲内のみとする。
def cos( x )
result = 0.0
for n in 0..10
result += (-1)**n * (x ** (2 * n)) / factorial( 2 * n )
end
return result
end
# 検算用
pi = 3.141592
n = 16
for i in 0..n
angle = ( 2.0 * pi * i.to_f / n.to_f ) - pi
printf("cos(%15.12f) = %15.12f\n", angle, cos( angle ) )
end
sin や cos、exp といった数学関数は頻繁に使用される。 かといって自分でこういった関数を書くのは骨の折れる仕事だ。 多くの言語ではよく使われる数学関数を簡単に使えるようにする仕組みが用意されている。 Ruby では、数学ライブラリ Math
4 を include すると、これらの数学関数を使えるようになる。 これを使うと以下のようにいとも簡単に計算ができてしまう。
# cos2.rb
include Math # 数学ライブラリを読み込む。
print sin( PI / 3.0 ), "\n" #=> 0.866025403784439
print cos( PI / 4.0 ), "\n" #=> 0.707106781186548
数学関数に限らず、言語側で用意されているものは敢えて自作しようとせず、 積極的に利用すると良い。
メソッドによって機能の区切りを定義すると、 その部分は他と独立して実行される機能となる。 そのため、 メソッドの中と外では、 変数を相互に参照・代入することができない。 仮に同じ名前のものがあっても、 別物であると判断される。
以下のプログラムでは、 print 文を実行したときの挙動を 「#=>
」 に続けてコメントとして記述してある。
# 定義ここから
def method1
#print "At line A: ", a, "\n" #=> undefined local variable or method `a'
a = 1
print "At line B: ", a, "\n" #=> At line B: 1
end
# 実行ここから
#print "At line C: ", a, "\n" #=> undefined local variable or method `a'
a = 0 # aを定義した
method1 # メソッドを実行
print "At line D: ", a, "\n" #=> At line D: 0
実行したときのプログラムの挙動は以下のようになる。
l.2 〜 l.6 はメソッドの動作を定義しているだけで、この段階では実行されない。
l.9 を有効にしていた場合、 a への値の代入がこれまでに実行されていないので、 「undefined」としてエラーになる。
l.10 で a に値 10 を代入している。
l.11 定義してあった method1 を実行する。 (l.2 にジャンプすると思えば良い。)
l.3 を有効にしていた場合、 a への値の代入がこれまでに実行されていないので、 「undefined」としてエラーになる。 ここの a はメソッド内の a であり、メソッド外の a (l.10) とは別物である。
l.4 でメソッド内変数 a に値 1 を代入。
l.5 でメソッド内変数 a の値を表示。
l.6 メソッド終了。呼出し元(l.11) に戻る。
l.12 メソッド外変数 a の値を表示。
変数がどこからどこまで見えるか、 を変数のスコープという。 スコープの範囲に無い場合は、 変数は存在しないことになる。
変数の前に$
マークを付けると、 その変数はグローバル変数と呼ばれ、 どのメソッドからも参照・代入可能となる。 言い方を変えると、 グローバル変数のスコープはプログラム全体である。
「変数のスコープで悩むくらいなら、 変数全部をグローバル変数にしてしまえば良いだろう」などと思うかもしれない。 しかし、 「グローバル変数はできる限り使わない」と覚えて欲しい。 プログラムの機能をメソッドに分けるのは、 機能を分業することに意義がある。 大規模なプログラムを作るとき、 分業して他の部分と切り離して独立して考えることができることは非常に重要である。 ところが、グローバル変数はその境界を曖昧にしてしまうのである。
メソッドとはプログラム本体の処理から切り分けられた 何らかの処理を担うものである。 これは現実世界における「材料を与えて製品を生産する工場」に喩えることができる。 今、肉まんを作っている工場があるとする。 ある日、この肉の中にメラミン 5 が混入していることが発覚した。 さあ、このメラミンはどこで混入したか、誰に責任があるか、 どうしたらメラミンが混入しないよいうに改善できるか? プログラミングにおいてグローバル変数のみで構成された世界とは、 この工場に世界中の誰もが自由に入って誰もがラインをいじれる世界である。 ゴミ回収業者が手を洗わずに生地に触ったり、 ダンボールなどの異物を混入させるかもしれないし、 ライバル会社が蒸し器の電源を落とすかもしれない。 プログラミングにおいて適切なスコープで切り分けられた世界とは、 処理に無関係な人が工場に入れないようにする世界である。 どちらの方が良い物を作れるか、問題が生じた時に対処し易いかは明白である。
12-scope1.rb の 変数a
を$a
に変えたものが以下の 12-scope2.rb である。 両者を実行し、出力を確認せよ。
# 定義ここから
def method1
print "At line A: ", $a, "\n" #=> 0
$a = 1
print "At line B: ", $a, "\n" #=> 1
end
# 実行ここから
print "At line C: ", $a, "\n" #=>
$a = 0 # aを定義した
method1 # メソッドを実行
print "At line D: ", $a, "\n" #=> 1
本節の内容はやや高度で、本演習で想定している範囲を越える。 演習を済ませたあと、時間と興味があれば読む程度で良い。
Ruby でメソッドと呼ばれているものは、 プログラミングの世界で関数と呼ばれるものとほぼ同じである。 メソッドという用語は元々オブジェクト指向プログラミングの世界での用語である。 Ruby はプログラムの構成全てがオブジェクトになるように設計された言語であり、 全ての関数がメソッドであると言えることから、 メソッドという用語が広く使われている。
厳密には、メソッドは「メンバ関数」と同一である。 メンバ関数とは何らかのオブジェクトに属する処理方法のことである。 なのでオブジェクトに属しない関数(メンバ関数でない関数)はメソッドではない。 Ruby ではそのような関数は存在しないが、 C++ などの言語ではそのような関数も存在する。
初学者で上記の意味がよく分からないのならば、 「ほぼ同じものだ」と考えて適宜読み替えても大きな問題はない。
今回で学んだメソッドは、 「引数に何らかの変数を与えれば、それに対応した一意の値を返す」という形式を取り、 数学で慣れ親しんだ関数と同じ感覚で扱える。 しかしプログラミングにおいて「関数」とは、 ひとまとまりの処理の単位という意味合いが強い。 前回学んだ段階でのメソッドはこのような意味合いのものである。 Ruby に組み込まれているメソッドでは printf()
が、 そのようなメソッドの例として挙げられる。 printf()
は「画面に出力という処理」をひとまとめにしたメソッドである。 我々が printf()
を使う場面では、 メソッドによって返ってくる値に興味があるのではなく、 「printf()
によって画面に文字列が出力される」という処理に興味がある。
本演習では Ruby を使ったプログラミングを学んでいるが、 別の言語、特に C言語ではその性格が顕著である。 C 言語では基本的に全ての処理が関数の動作として定義され、 その関数を呼ぶことで様々な処理を行う。 「どのように処理を関数ごとに切り分けて、どのような関数を作るか」を 考えることがプログラミングに向き合う基本的なスタンスであると言える。 C言語ではベンダ提供の多くの関数が存在し、 この関数の使い方を学ぶのが初学者の一つのステップにもなっている。
Ruby での正統なプログラミングスタイルは、 このように関数ベースで処理を考えるというものではない。 Ruby はオブジェクト指向をもとに設計された言語であり、 オブジェクト指向プログラミングではクラスにメソッドを作用させることで処理を進める ことが基本である。 しかし、クラスに付属するメソッドとしての定義方法は範囲外とする。 オブジェクト指向でどうプログラミングするのかという、より発展的な内容を含むため、 半期では授業時間が足りないからだ。 興味のある人は自分で調べられたい。
1つの関数は、エディタを画面最大化しておおよそ1画面におさまるくらいを目安にするのが良い。 何百行もかかる関数は、関数の中でどこにどういう記述をしたのか、 パッと見通しがつきにくいからだ。 それを越えるようなら複数の関数に分割できないか検討すること。
慣れないうちは、1つ関数を作るごとに動作をテストすること。 プログラムが何か問題を起こしたとき、 その関数の動作が完璧であることが確認できているのならば、 問題はそれ以外の場所にあるのだから。
# factorial1.rb
result = 1
for i in 1..10
result *= i
end
print result, "\n" #=> 3628800
プログラミングをするときに、 コピー&ペーストをするという行動は、 何かがおかしいという予兆である。 プログラミングでは基本的に、同じコードを二度以上書くべきではない。 たとえばコピーして 100 回ペーストした部分に修正を加える必要があった場合には やっぱり100箇所修正する必要がある。 非常に気の滅入る作業だ。 コピー&ペーストそのものを悪というべきではないが、 同じコードが複数箇所に現れるということがそのプログラムの品質を落とす。 コピー&ペーストをするのは将来の自分を苦しめる行為であると認識して欲しい。 コピー&ペーストが必要に見えるときは何かがおかしいと気付くセンスを持って欲しい。