情報科学 2007 久野クラス ＃ 7
久野靖∗

2007.11.30

はじめにさて皆様、駒場祭はいかがでしたか。もう年末が近いので、年内に頑張って来年になってからは楽をするということで、あとちょっと頑張ってください。今回は数値解析で最後に残った話題である「常微分方程式の数値解法」を扱い、その後ようやく、Ruby の特徴である「オブジェクト指向」の話題に入ります。

1

演習問題解説

1.1

演習 1a: Gauss-Jordan の消去法

演習 1a は、Gauss-Jordan の消去法のプログラムを作ることだった。これは Gauss 法を削ってちょっと直せばできる。

def gaussjordan(a)n = a.lengthn.times do |i|n.times do |j|if i != j thenr = a[j][i] / a[i][i].to_f
i.step(n) do |k| a[j][k] = a[j][k] - a[i][k]*r endendend# p(a)
end
n.times do |i| a[i][n] = a[i][n] / a[i][i] end
return aend
要するに、内側ループを i 以降ではなく 0 番から全部やり、ただし i 番は飛ばすために if で囲んだ。あと、消去しただけだと係数が 1 になっていないので、最後に係数で割る処理を 1 行入れてある。

1.2

演習 1b: ピボット選択

ピボット選択とは何だったかというと、xi を消去しようとしたところで i 列の xi の係数が 0(かそれに近い) だとうまく行かないので別の列と入れ替えて進めるという話だった。で、いちいち判断するのでなく、xi を消去するときに常に係数の絶対値が最大の列を持って来てそれを使うようにした。ところで、係数の絶対値が最大の列を持って来たのに、その係数が 0(かそれに近い) だったら? それは方程式が不定/ 不能だということで、ついでに演習 1c も組み込んである。
∗ 筑波大学大学院経営システム科学専攻

1Ｇａｕｓｓの消去法
a
a
a
a

00
10
20
30

a
a
a
a

01
11
21
31

a
a
a
a

02
12
22
32

Ｇａｕｓｓ−Ｊｏｒｄａｎの消去法
a
a
a
a

03
13
23
33

b
b
b
b

a

0

a

1

a

2

a

3

下側のみ消去

00
10
20
30

a
a
a
a

01
11
21
31

a
a
a
a

02
12
22
32

a
a
a
a

03
13
23
33

b
b
b
b

0
1
2
3

下・上とも消去

図 1: Gauss-Jordan の消去法

def gaussjordanpivot(a)n = a.length; idx = Array.new(n) do |i| i end
n.times do |i|k = selectpivot(a, i, n)
if a[k][i].abs < 0.00000001 then return nil end
a[i],a[k] = a[k],a[i]; idx[i],idx[k] = idx[k],idx[i]n.times do |j|if i != j thenr = a[j][i] / a[i][i].to_f
i.step(n) do |k| a[j][k] = a[j][k] - a[i][k]*r end
endend
p(a, idx)
endb = Array.new(n)n.times do |i| k = idx[i]; b[k] = a[k][n] / a[k][k] end
return bend
つまり、下請け関数 selectpivot で i 列目以降 (この手前はもう消去に使ってしまったので除外) で xi 項の係数が最大の列番号 k を得て、もしその係数が 0 なら不定/不能の印として nil を返す。そうでない場合は i 列と k 列を交換してこれまで通り進める。
ただしこれをやってしまうと、列がバラバラに入れ替わってしまうので、最後に解を出力するときどの変数が何番目に入っているか分からなくなって困る。このため、最初に配列 idx を用意してここに 0〜n − 1 の番号を順に入れておき(このヘンな Array.new は初期化時に添字 i を渡されて任意の式により初期値を計算させてくれるというもの)、ここでは要するに [0,1,2,...,n-1] で初期化)、ピボット選択の交換時にこの配列も同じに交換することで、元の番号が分かるようにし、最後に結果用の配列 b を用意してここに元の番号で書き込むようにしている。

1.3

演習 2: 反復法

2a と 2b については色々入れて観察するだけですが、要は対角成分が大きいと収束しそうでないと収束しないとか、正しい解の回りを振動しつつ収束したり発散したりするとか、分かればいいのではないでしょうか。いちおう、Jacobi 法のコードを再掲:

def jacobi(a)n = a.length; x = Array.new(n, 0); count = 0
while true do
x1 = Array.new(n, 0); d = 0.02

n.times do |i|v = a[i][n].to_f
n.times do |j| if j != i then v = v - a[i][j]*x[j] end end
x1[i] = v / a[i][i]; d = d + (x1[i]-x[i]).abs
end
x = x1; count = count + 1# p(x)
if d < 0.000000001 then return x end
if count > 100 then return nil end
end
return x
end
これを Gauss-Seidel 法にするには、要は配列 x1 を使わずに x だけで済ませるように直せばよい。そのとき、収束判定のための変数 d に xi の変化の絶対値を累計するので、作業変数を 1 つ用意してここに計算し、差の絶対値を累計してから x に入れる程度。

def gaussseidel(a)n = a.length; x = Array.new(n, 0); count = 0
while true do
d = 0.0
n.times do |i|v = a[i][n].to_f
n.times do |j| if j != i then v = v - a[i][j]*x[j] end end
z = v / a[i][i]; d = d + (z-x[i]).abs; x[i] = z;
end
count = count + 1
p(x)if d < 0.00000001 then return x end
if count > 10 then return nil end
end
return x
end
さて、収束はどうだろうか。100 だと多くて大変なので、10 回でやめるようにして経過を出力させ、前回のデータで動かしてみる。まず Jacobi 法。

irb(main):008:0> jacobi([[5,2,-1,19],[2,-3,2,-1],[1,1,-2,8]])[3.8, 0.333333333333333, -4.0][2.86666666666667, 0.2, -1.93333333333333][3.33333333333333, 0.955555555555555, -2.46666666666667][2.92444444444444, 0.911111111111111, -1.85555555555556][3.06444444444444, 1.04592592592593, -2.08222222222222][2.96518518518518, 0.988148148148148, -1.94481481481482][3.01577777777778, 1.01358024691358, -2.02333333333333][2.9899012345679, 0.994962962962962, -1.98532098765432][3.00495061728395, 1.00305349794239, -2.00756790123457][2.99726502057613, 0.998255144032921, -1.99599794238683][3.00149835390947, 1.00084471879287, -2.00223991769547]=> nil3

続いて Gauss-Seidel 法。

irb(main):009:0> gaussseidel([[5,2,-1,19],[2,-3,2,-1],[1,1,-2,8]])[3.8, 2.86666666666667, -0.666666666666667][2.52, 1.56888888888889, -1.95555555555556][2.78133333333333, 0.883851851851852, -2.16740740740741][3.01297777777778, 0.897046913580247, -2.04498765432099][3.0321837037037, 0.991464032921811, -1.98817613168724][3.00577916049383, 1.01173535253772, -1.99124274348423][2.99705731028807, 1.00387637786923, -1.99953315592135][2.99854281766804, 0.999339774497789, -2.00105870391709][3.00005234941747, 0.999329097000254, -2.00030927679114][3.00020650584167, 0.999931486033688, -1.99993100406232][3.00004120477406, 1.00007346714116, -1.99994266404239]=> nil
こうしてみると、解 (3,1,-2) への収束は後者の方がかなり速いことが分かる。各周回において、おおむね後者は前者の誤差を ǫ とすると、後者の誤差は ǫ2 くらいな感じである。
乱数の演習の解説は省略させてください。ゲーム作るのがメインだろうし。

2

常微分方程式の数値解法

2.1

常微分方程式

常微分方程式とは、未知関数とその導関数から成る等式で定義される方程式 (微分方程式) のうち、未知関数が (本質的に)1 つの変数を持つ場合を言う。導関数が 2 次以上の時は高階常微分方程式、方程式が複数ある場合は連立常微分方dy= f (x, y) の形を持つものを扱う。
程式と呼ぶ。ここでは一番簡単な場合として dxyx

図 2: 常微分方程式と初期値問題
直観的に説明するなら、XY 平面の至るところにおいて傾きを表す矢印が定義されていて、その方向に沿った曲線を求める問題、という風に考えることができる (図 2)。もちろん、そのような曲線は無限にあるが (一般解)、任意の点 (x0 , y0 )を与えてそこを通る曲線を求めるとすれば、その曲線は 1 つだけになる (特殊解)。この、初期値を与えて特殊解を求める問題を初期値問題と呼ぶ。
簡単な例として、次の常微分方程式を考える。

1
dy
=
dx
2y

4

これを次のように変形する。

2y dy = 1 dx
両辺を不定積分する。

Z

2y dy =

Z

1 dx

両辺とも積分の結果次のようになる。

y2 = x + C
よって次の式が求まり、これが一般解となる (もちろん y = 0 は除く)。

y = ±

√
x+C

そして、たとえば点 (1, 0) を通る特殊解を求めるなら、上式の y に 1、x に 0 を代入して C = 1 が求まるから y =√x+1が解となる。

Euler 法2.2

前節のような簡単な常微分方程式の初期値問題は解析的に (数式の形で) 解が求まったが、現実の問題では解析的には解が求められないことが多い。そのような場合に、(数値積分と同様に) 数値的に解を求める方法 (数値解法) が使われる。
dy= f (x, y) として分かっているわけなので、一番素朴な方法として、初期値 (x0 , y0 ) における解曲線の接線の方向は dx

この方向に微小な値 h ぶんだけ動いた点を (x1 , y1 ) とし、以下同様にして次々に曲線上の値を (近似的に) 求めて行くという方法が考えられる。これを Euler 法と呼ぶ。整理すると、次の式で xi , yi を計算していくのが Euler 法である。

xi+1 = xi + h
yi+1 = yi + h f (xi , yi )これを使って x が指定値になるまで指定した分割数で計算して最後の y の値 ((0, 1) から始めた場合√
x + 1 になるはず)を返す Ruby プログラムを示す。

def euler(xmin, y, xmax, count)h = (xmax-xmin).to_f / count
count.times do |i|# x = xmin + h * (i+1)
y = y + h * 0.5/y # f(x,y) = 1/2y
end
return yend
なお、この方程式の場合は x を使わない (f (x, y) が x に依存しない) ので、x の計算はコメントアウトしてある。
実際に動かしてみよう。x として 1 を指定することで、2 の平方根を計算するようにし、刻み数を変えることで精度の変化を見ている。

irb(main):003:0> euler(0,1,1,100)=> 1.41482796736591
irb(main):004:0> euler(0,1,1,1000)=> 1.41427484589246
irb(main):005:0> euler(0,1,1,10000)=> 1.41421968916029
まあ、あんまりよくはない。覚えていない人のため、1 桁の平方数でない数の平方根を掲げておく。

5√2 ≈ 1.4142135623730950488016887242096980785696√
3 ≈ 1.7320508075688772935274463415058723669428√
5 ≈ 2.2360679774997896964091736687312762354406√
6 ≈ 2.4494897427831780981972840747058913919659√
7 ≈ 2.6457513110645905905016157536392604257102√
8 ≈ 2.8284271247461900976033774484193961571393

double 型の精度は 16 桁くらいなので、もうちょっと精度良く求まって欲しいわけだが… Euler 法の場合、「個々の点における」接線を延長して次の点を求めているため、微分の向き (矢線の向き) が揃っている (まっすぐな) 所ではいいけれ
ど、カーブしているところではどんどん「外側」にずれて行ってしまう。これを補う方法を次に述べる。

2.3

Runge-Kutta 法

Euler 法では各ステップの始点における接線を延長して次の点を求めていたが、それでは解曲線がカーブしているときに誤差が大きい。そこで、始点と終点の両方で接線の向きを求め、それを平均すればより正確になると考える。しかし実際には、各ステップの終点は「これから求める」ので分からない。そこで Euler 法で終点の近似値を求め、そこの傾きを使い、これと始点での傾きとの平均を取る (図 3)。つまり次のようにするわけである。

xi+1 = xi + h
k1 = h f (xi , yi )k2 = h f (xi + h, yi + k1)
yi+1 = yi + 12 (k1 + k2)
Ｅｕｌｅｒ法２次のＲｕｎｇｅ−Ｋｕｔｔａ法

傾きは始点とは違っているはずステップの終点

仮のステップの
終点

ステップの
終点
ステップの
始点

ステップの
始点

始点と仮の終点の傾きの平均を取る

図 3: Euler 法と 2 次の Runge-Kutta 法
これを 2 次の Runge-Kutta 法と呼ぶ。これを Ruby プログラムにしたものを示しておく。

def rungekutta2(xmin, y, xmax, count)h = (xmax-xmin).to_f / count
count.times do |i|# x = xmin + h * (i+1)
k1 = h * 0.5/y
k2 = h * 0.5/(y+k1)
y = y + 0.5*(k1+k2)
end
return yend
これによる計算を示しておく。ずっと精度がよくなっていることが分かる。

6

irb(main):013:0> rungekutta2(0,1,1,100)=> 1.41421356445272
irb(main):014:0> rungekutta2(0,1,1,1000)=> 1.41421356237517
irb(main):015:0> rungekutta2(0,1,1,10000)=> 1.41421356237309
さらに正確さを増すため、h の半分だけ上記の方法で進み、そこから終点までを 2 番目の近似値で進み、始点、最初の近似値、2 番目の近似値、終点の近似値を 1:2:2:1 の比率で混ぜる方法があり、4 次の Runge-Kutta 法として知られている。単に Runge-Kutta 法と言った場合はこちらを指すことが多い。

xi+1 = xi + h
k1 = h f (xi , yi )k2 = h f (xi + h2 , yi + k1
2 )k3 = h f (xi + h2 , yi + k2
2 )k4 = h f (xi + h, yi + k3)
yi+1 = yi + 61 (k1 + 2k2 + 2k3 + k4)
なぜ 1:2:2:1 の比率がいいかとかはここでは説明しないが、Taylor 展開による 1 次の項のあてはめが Euler 法、2 次の項まであてはめるのが 2 次の Runge-Kutta 法、4 次の項まであてはめるのが 4 次の Runge-Kutta 法である。
ついでに誤差についてもおおまかに説明すると、Euler 法では各ステップの誤差が (1 次まであてはめた残りなので)O(h2 )、それを h1 ステップ累計するため全体の誤差は O(h) となる。同様に 2 次の Runge-Kutta 法では各ステップ O(h3 )、全体
で O(h2 )、4 次の Runge-Kutta 法では各ステップ O(h5 )、全体で O(h4 ) となる。これを言い替えると、h を 12 にしたとき Eular 法では全体の誤差がほぼ 12 になるのに対し、2 次の Runge-Kutta 法では 212 、4 次の Runge-Kutta 法では 214 になる。
なお、刻みをどんどん細かくする程いいかというと、細かくする程計算時間が掛かるだけでなく、丸め誤差や情報落
ち誤差が累積するので、結局精度もある程度以上は上がらなくなる。このため、2 次や 4 次の Runge-Kutta 法のように最初から性能のよい公式を使うことは大変有利だと言える。
演習 1 Eular 法または 2 次の Runge-Kutta 法のプログラムを打ち込み、これらでさまざまな値の平方根を刻み幅を変えて計算し、上で説明した誤差に関する性質が成り立っているかどうか確認してみよ。
演習 2 さらに 4 次の Runge-Kutta 法のプログラムも用意し、上と同様に検討せよ。

3

オブジェクト指向

3.1

オブジェクト指向とは

皆様はここまでに、配列、レコードなどのデータ構造を用意し、それを操作するメソッドを複数組み合わせてアルゴリズムを実現する、という形でプログラムを作成してきた。最初の方で説明したように、このようなスタイルを手続き
型のプログラミングスタイルという。
手続き型のプログラミングスタイルは長い間主流として使われて来たが、近年のようにプログラムが大きくなり複雑化してくると、次のような弱点が問題になってきた。

• 「データ構造」と「手続き」が分離しているので、どの手続きがどのようなデータ構造を扱う、というのがごちゃ
ごちゃになりやすい。
• 手続きが多数になると、どの手続きが何だったのかの構造を憶えて管理するのが大変になる。
• データ構造の内容がどこからでも分かりアクセス可能なので、本来なら構造を直接いじらない (専用の手続きに頼む) ところでまちがっていじってしまいトラブルをまねくことがある。

7

オブジェクト指向 (Object-orientation, O-O) とは、これらの弱点を克服すべく手続き型言語を拡張した概念であり、今日では多くのプログラミング言語に取り込まれている。その要点は何かというと、プログラムが扱うデータをさまざ
まな「もの」として扱うことにある。
我々が日常扱っている「もの」にはそれぞれ固有の機能や特性があり、我々は「内部構造」には関わらなくてもこれらの「もの」の機能や特性を活用することができる。たとえばイスであれば「座る」「高さを調節」「移動させる」などの操作ができるし、ペンであれば「キャップをつける/外す」「描く」などの操作ができるし、それぞれ固有の色などもある。しかしこれらを利用したり参照するのに、イスやペンの内部構造を理解している必要はない。

3.2

クラスとインスタンス

では次に、前節で述べたような「もの」を言語上でどのように表すかを考えてみまよう。1 つの素直な考え方として、ものには「種類」(クラス) があるものと考え、その種類ごとに「どんな性質を持つか」「どのような操作ができるか」などを定義していく、というものがある。このような考え方に従うオブジェクト指向言語をクラス方式のオブジェクト指
向言語、と呼ぶ。Ruby も Java も C++もクラス方式のオブジェクト指向言語である。
では次に、ものの種類の定義、つまりクラス定義には何が含まれるべきかを考えてみよう。先に述べたように、それぞれの「もの」には固有のデータと固有の操作があるので、それを「変数」と「メソッド」で表すのが自然だといえる。
ただし、ここでいう変数とメソッドは、これまで使って来た変数やメソッドとは少し違っている。つまり、あるクラスを定義して、そのクラスに所属する「もの」(プログラミング言語の言葉ではこれをインスタンスないし「実体」と呼びます) を定義したとすると、クラスで定義した変数やメソッドは「そのクラスのインスタンスに対して適用される」ものとなります。これをインスタンス変数、インスタンスメソッドと呼ぶ。
クラス定義
class Xyz
def m0(p, q)@a = ...
end
def m1(n)...@b...
end
end

x1 = Xyz.new(...)
x2 = Xyz.new(...)

x1:

x2:
@a
@b
m0
m1

@a
@b
m0
m1

x1.m0(10, 20)x2.m1(100)

図 4: クラスとインスタンス

たとえば図 4 では、クラス定義 Xyz を「ひな型」として 2 つのインスタンスを生成し、変数 x1 と x2 に入れている。
このとき、この 2 つのインスタンスは内部に持っているインスタンス変数群も使用できるメソッド群も同じ (つまりクラス定義に書かれている通り) であるが、インスタンスとしては別個、つまりインスタンス変数群はそれぞれ別個になっている。
ここで、メソッド呼び出し記法により「x1.m0(...)」「x2.m1(...)」のようにそれぞれのインスタンスメソッドを呼び出したとすると、それらのインスタンスメソッドの中でインスタンス変数を参照したときは、それぞれ自オブジェク
トのインスタンス変数が使われることになる。
よく使われる「動物」の例で考えると、「犬」というクラスを作って、そこで「名前」「走っている速さ」というインスタンス変数を持たせたとすると、どの犬もこれら 2 つのインスタンス変数を持っているという点は同じだが、そこに格納されている値、つまりそれぞれの犬の名前やそれぞれの犬の走っている速さはどの犬かによって、つまりインスタ
ンスによって違う、というわけである。

8

Ruby による簡単なクラスの定義

3.3

では Ruby の場合について説明しよう。まずクラスは次の構文により定義する。

class クラス名...
end
クラス名は必ず英大文字で始めることになっている。そして、この中にメソッドの def を書くと、自動的にインスタン
スメソッドになり、「オブジェクト. メソッド名 (...)」で呼び出せる (この書き方は既に Array の push だとか Numberの times だとか山のように使っていたが、それが自分で定義できるということ)。最後に、インスタンス変数はこれまでの変数と異なり、名前の最初が「@」で始まる。以上です。
おっとあと 1 つだけ。クラスからインスタンスを作るにはクラス名.new(...)という特別なメソッド呼び出しを使うが、このときもしインスタンスメソッドの中に initialize という名前のものがあればそれが呼び出され、その時 new に渡したパラメタがそっくりそのまま渡されて来る。つまりまさに、初期化のためにこのようになっているわけだ。
では、非常に簡単なクラスの定義を見てみよう。これは上で挙げた「犬」のクラスの例である。

class Dog
def initialize(name)@name = name; @speed = 0.0
end
def talkputs(’my name is ’ + @name)end
def addspeed(d)@speed = @speed + dputs(’speed = ’ + @speed)
end
end
initialize では name(名前) を受け取り、その値でインスタンス変数@name を初期化する。インスタンス変数@speed は0 に設定します。メソッド talk では自分の名前を喋る (喋る犬?)。addspeed では渡された値だけスピードを増してそれを表示する。動かしてみよう。
irb(main):021:0> a = Dog.new(’pochi’)=> #<Dog:0x81b5b2c @name="pochi", @speed=0.0>
irb(main):022:0> b = Dog.new(’tama’)=> #<Dog:0x81b049c @name="tama", @speed=0.0>
irb(main):023:0> a.talk
my name is pochi=> nilirb(main):024:0> b.talk
my name is tama=> nilirb(main):025:0> a.addspeed(5.0)
speed = 5.0=> nilirb(main):026:0> b.addspeed(8.0)
9speed = 8.0=> nilirb(main):027:0> a.addspeed(10.0)
speed = 15.0=> nilポチのインスタンスとタマのインスタンスは別のもので、名前や速度を別個に持っていることが分かると思う。このよ
うに「もの」単位で扱えるところがオブジェクト指向の特徴だということ。
演習 3 この例題を打ち込んでそのまま動かしなさい。動いたら、「ほえる」メソッド bark(引数は無し) と、「ほえる回数」を設定するメソッド setcount(引数は回数) を追加しなさい。とくに設定しない場合は 3 回ほえるものとしま
す (もちろん、ほえる回数を憶えるインスタンス変数を追加する必要もあるでしょう)。

3.4

例: 有理数クラス

ワンと吠える犬ではあんま役に立ちそうな気がしないので、もう少し有用なものを作って見ることにしよう。これまで、整数でない数値計算には誤差がつきものだという話をさんざんして来ましたよね。では、浮動小数点計算をするのではなく、割り算をしたときに「分子, 分母」の分数の形で保持すれば誤差なしで済む。もちろん、加減算のときは通分して計算する。そのようなクラスを作ってみた。

class Rationaldef initialize(a = 1, b = 1)
@a = a; @b = b
if b == 0 then @a = 1; return end
if b < 0 then @a = -a; @b = -b end
if a == 0 then @b = 1
elsif a > 0 then @a = a / gcd(a,b); @b = b / gcd(a,b)
else
end

@a = a / gcd(-a,b); @b = b / gcd(-a,b)

end
def getDivisor
return @b
end
def getDividend
return @aend
def to_s
return "#{@a}/#{@b}"end
def +(r)return Rational.new(@a*r.getDivisor+r.getDividend*@b, @b*r.getDivisor)end
def gcd(x, y)while true do
if x > y then x = x % y; if x == 0 then return y endelse
y = y % x; if y == 0 then return x end
end
end
end
10

end
つまりインスタンス変数@a と@b に分子と分母をそれぞれ保持しておくわけだ。initialize のパラメタに代入が書いてあるが、これは「デフォルト値」つまりそのパラメタを省略した場合はこの値を使ってねという意味になる。だから「Rational.new(3)」で 13 になるわけだ。その後 initialize の中身がごちゃごちゃしているが、これは (1) 分母が 0 のと
き (不定) は分子を 1 とする、(2) 分母は 0 でないなら常に正とする (負の数は分子が負)、(3) 値ゼロは 01 で表す、(4) 必ず既約分数にする、という正規化 (なるべく形を揃えること) を行っているから。メソッド gcd はご存じの通りのものが末尾に入れてある。
次に、分母だけ、分子だけを取り出したい場合のために、メソッド getDivisor、getDivdend を用意した。このような、インスタンス変数をアクセスするだけのメソッドのことをアクセサと呼ぶ。逆に言えば、アクセサがなければイン
スタンス変数の内容は外部からは参照できない。このようにしておくと、後で表現方法を変えた時にも外部に影響が及ばないなどの利点がある。
また、文字列への変換メソッド to s も用意した。これは puts などによる打ち出しなどの時に自動的に「a/b」という形の文字列を生成できるので用意しておくと便利である。
そして、演算としてはとりあえず加算だけ用意した。加算はやはり「+」で表したいので、メソッド名を「+」にしてある。このようにして、演算子を定義できるのは Ruby の特徴の 1 つである (もっとも C++などでもこれができるが)。
では動かしてみよう。

irb(main):004:0> a = Rational.new(3,5)=> #<Rational:0x81f978c @b=5, @a=3>
irb(main):005:0> puts a
3/5=> nilirb(main):006:0> b = Rational.new(8,7)=> #<Rational:0x81f00d8 @b=7, @a=8>
irb(main):007:0> puts b
8/7=> nilirb(main):008:0> puts a+b
61/35=> nil
確かに、通分して計算してくれている。
演習 4 有理数クラスをそのまま打ち込んで動かせ。動いたら、四則の他の演算も追加し、動作を確認せよ。できれば、これを用いて浮動小数点では正確に行えない「実用的な」計算が正確にできることを確認してみよ。
演習 5 複素数を表すクラス Complex を定義し、動作を確認せよ。これを用いて何らかの役に立つ計算をしてみられるとなおよい。
演習 6 クラス定義を活用した Ruby「面白い」プログラムを作って動かせ。面白さの定義は各自に任されるものとする。

A

本日の課題 7A

「演習 1」〜「演習 3」から 1 つ選び、今日中に久野までメールで送ってください。

1. Subject: は「Report 7A」とする。
2. 学籍番号、氏名、投稿日時を書く。
3. 「演習 1」〜「演習 3」で動かしたプログラムどれか 1 つのソース。
4. 検討結果のまとめ (とできれば簡単な考察)
5. 以下のアンケートの回答。
11

Q1. 常微分方程式の数値解法について納得しましたか。
Q2. クラスの概念やその機能について納得しましたか。
Q3. 本日の全体的な感想と今後の要望をお書きください。

B

次回までの課題 7B

次回までの課題は「演習 1」〜「演習 6」(の、 7A で提出していないもの) から 2 つ以上選んで報告することです。
各課題のために作成したプログラムはすべてレポートに掲載してください。レポートは授業開始時刻の 10 分前 までに久野までメールで送付してください。

1. Subject: は「Report 7B」とする。
2. 学籍番号、氏名、投稿日時を書く。
3. 1 つ目のプログラムのソース。
4. その説明と分析/考察。
5. 2 つ目のプログラムのソース。
6. その説明と分析/考察。
7. 下記のアンケートの回答。
Q1. クラス定義が書けるようになりましたか。
Q2. この先、このクラス定義の機能がどのように使われると予想しますか。
Q3. 課題に対する感想と今後の要望をお書きください。

12