授業資料/11 の変更点


#contents

//  第 11 回 (Ruby 関数)

* インデント(行頭からの字下げ): 細かいテクニック [#h80bebec]

&ref(/materials/warning.png); 
これまでのサンプルプログラムなどで,''for''文や ''if'' 文の中身が適切に「行頭から字下げ(インデント)」されていることに着目しよう.

これはその文などの''「有効範囲」''を示す,古くから伝わるベタなテクニックである.

そして,これによって,インデントを見ただけでプログラムの構造がわかる.
こうしておけば,「end がありません」というエラーや,「どこまでが for の範囲だろう」などの悩みなどと無縁になれる.
つまり,
&br;&br;
CENTER:&size(24){''適切なインデントが,まっとうなプログラミングの第一歩''};
&br;&br;

なのである.

インデントに手間がかかるのでは? という疑問もあるだろうが,エラーに悩む時間の長さを考えれば,インデントの手間など微々たるものだ.だから,次のようにも言ってもよいだろう.
&br;&br;
CENTER:&size(24){''初心者はエラーに悩む前にきちんとインデントすべし.''};
&br;&br;

&br;&br;
* プログラムの機能を部品化する [#hc2f7e4d]

さて,今回はプログラムの機能を部品扱いできるというとても重要な話だ.
これができないと,プログラムはたちまちだらだらと長くてわかりにくいものとなり,扱い難いリヴァイアサンと化してしまうだろう.


** 機能単位として関数を定義: def [#m871af12]

一定の機能を果たす小さなプログラム部分を,関数として定義する方法を学ぼう.

文法は簡単で,

&br;&br;
&size(24){''def'' &color(blue){関数名}; ''('' &color(blue){入力変数名1, 入力変数名2, ...}; '')''};&br;
    &size(24){&color(blue){この関数の動作, 計算など};};&br;
    &size(24){''return'' &color(blue){出力値};};&br;
&size(24){''end''};
&br;&br;

となる.青字の部分は自分で変えられるところだ.

&ref(/materials/warning.png); 関数の「入力変数名」はその関数定義の中で「のみ」有効だ.数学っぽいね.

&br;&br;
&ref(/materials/notes.png); 実習: 以下のサンプルを動かしつつ,理解しよう.

数字の集合を与えるとその平均を返す関数を作ってみる.

まず,関数部分だけ書いてみると,次のようになるだろう.
// programu source 表記
#highlighter(language=ruby,number=on,cache=on){{
def mean(a) # 入力として a をもらって…
  sum = 0.0
  for i in 1..a.size do
    sum += a[i-1] # 全部足しこんでいって…
  end
  return sum/a.size  #/ ←この2文字は web画面の色の都合でつけてるだけ.気にするな.
  # 平均を出力する
end
}}

もちろん,これをプログラムの中で使わないと意味が無い.
そこで,どう使えるのか,下の例を見てみよう.
// programu source 表記
#highlighter(language=ruby,number=on,cache=on){{
include Math

# 集合平均値を返す関数
def mean(a)
  sum = 0.0
  for i in 1..a.size do
    sum += a[i-1]
  end
  return sum/a.size #/
end

# 上で作った関数を使ってみよう.
# データを用意して…
x = [3, 23, 4, 8, 9]

# mean 関数を使ってみる!
p mean(x)
}}

実際にこのプログラムを動かすと,
//
  9.4

という結果が出てくるはずだ.

こんな感じで関数を作って使うことが出来る.

&br;&br;
** 関数のなにがありがたいの? 面倒なだけじゃ? [#vf4dd42c]

いやいや,そんなことはない.
次のような例を考えてみよう.

&br;&br;
&ref(/materials/notes.png); 実習: 以下のサンプルを動かしつつ,理解しよう.

数字の集合をもらってその平均を返す仕事をしよう.
ただし,与えられる集合がたとえば 5個ほどあるとしよう.

''def'' 機能を使わずにプログラムを書くとすると,次のようになるはずだ.
// programu source 表記
#highlighter(language=ruby,number=on,cache=on){{
include Math

a=[0, 14, 8, 0, 0]
sum = 0.0
for i in 1..a.size do
  sum += a[i-1]
end
p sum/a.size
#/

a=[10, 2, 1, 12, 18]
sum = 0.0
for i in 1..a.size do
  sum += a[i-1]
end
p sum/a.size
#/

a=[13, 3, 12, 17, 16]
sum = 0.0
for i in 1..a.size do
  sum += a[i-1]
end
p sum/a.size
#/

a=[13, 11, 14, 15, 19]
sum = 0.0
for i in 1..a.size do
  sum += a[i-1]
end
p sum/a.size
#/

a=[1, 13, 17, 18, 14]
sum = 0.0
for i in 1..a.size do
  sum += a[i-1]
end
p sum/a.size
}}


… 同じことを何回も書いていて,非常に馬鹿馬鹿しいことがよくわかる.
しかも,どこかを修正しようとしたら,修正漏れなどのミスが発生しやすいことも理解できるだろう.

&br;&br;
そこで,関数 ''def'' を使ってみた次の例を見てみよう.
// programu source 表記
#highlighter(language=ruby,number=on,cache=on){{
include Math

# 集合平均値を返す関数
def mean(a)
  sum = 0.0
  for i in 1..a.size do
    sum += a[i-1]
  end
  return sum/a.size #/
end

# データがいくつあっても簡単に平均がだせるね.
p mean([0, 14, 8, 0, 0])
p mean([10, 2, 1, 12, 18])
p mean([13, 3, 12, 17, 16])
p mean([13, 11, 14, 15, 19])
p mean([1, 13, 17, 18, 14])
}}

短く書けるだけでなく,なにより''「わかりやすい」''し,「同じことを書かずに済むので,修正ミスが発生しにくい」.

以上のことを踏まえてよく考えてみると,
&br;&br;
CENTER:&size(24){''同じことは書くな!''};
CENTER:&size(24){''そういう時は def を使って一回で!''};
&br;&br;

ということになる.

&br;&br;
** 練習問題 [#kd557568]
&br;

数字の集合を与えるとその''二乗平均''を返す関数を作ってみよう(各要素の二乗したもの,の平均).
そして,上のようにして,それを使ってみるサンプルプログラムを書いてみよう.

&br;&br;
** 練習問題 [#o73d39c9]
&br;

数字の集合を与えるとその平均を返す関数とその二乗平均を返す関数とを使って,
数字の集合を与えるとその''分散''を返す関数を作ってみよう.
サンプルについても同様に.

&br;&br;
** ''def'' を使って関数を作るときの,関数の出力値について [#l1e21a00]
&br;

関数の出力は基本「何でもあり」だ.
そして,その中で初心者が気づきにくいがとても便利なものに,''真偽値'' がある.
これは「Yes? No?」を判定するような関数としてとてもとても便利に使える.

では,まず先に,
&br;&br;
CENTER:&size(24){''Ruby では真は true で,偽は false で表す''};
&br;&br;

と覚えておこう.これを覚えたら,次のサンプルを見て理解しよう.

&ref(/materials/notes.png); 実習: 以下のサンプルを動かしつつ,理解しよう.

数字の集合を与えると,「その最後の要素が,平均より大きいか?」を判定する関数を作ってみよう.
先の平均を与える関数を利用すればとても簡単に次のように書ける.
// programu source 表記
#highlighter(language=ruby,number=on,cache=on){{
include Math

# 集合平均値を返す関数
def mean(a)
  sum = 0.0
  for i in 1..a.size do
    sum += a[i-1]
  end
  return sum/a.size  #/
end

# /集合の最後の要素が平均より大きい? を答える関数
def last_is_larger(a)
  if (a[a.size-1] > mean(a)) then
      return true
    else
      return false
    end
end

# ちょっと使ってみる
x = [3, 4, 8, 128, 16]
p x
p last_is_larger(x)
}}

真偽値を返すということがなんとなくでもわかったと思う.

&br;&br;
そして次は「これが何の役に立つのか?」である.
答えは簡単で,
&br;&br;
CENTER:&size(24){''真偽値を返す関数は,if 文の判断部分で使える''};
&br;&br;

のである.
それについては,次のサンプルを見てみよう.

&ref(/materials/notes.png); 実習: 以下のサンプルを動かしつつ,理解しよう.

数字の集合を与えると,「その最後の要素が平均より大きかったら大きさが平均である要素を最後尾に付け足し,そうでなかったら最後尾の要素を取り去る」という関数をつくろう.
これには,上のプログラム例に少し付け足せばできるはずだ.
// programu source 表記
#highlighter(language=ruby,number=on,cache=on){{
include Math

# 集合平均値を返す関数
def mean(a)
  sum = 0.0
  for i in 1..a.size do
    sum += a[i-1]
  end
  return sum/a.size #/
end

# 集合の最後の要素が平均より大きい? を答える関数
def last_is_larger(a)
  if (a[a.size-1] > mean(a)) then
      return true
    else
      return false
    end
end

# 「その最後の要素が平均より大きかったら大きさが平均である要素を最後尾に付け足し,
# そうでなかったら最後尾の要素を取り去る」関数.
def modify_array(a)

  b = a.dup   # 説明すると長くなるが,関数がもらった入力集合に「変更」を加えるときは
              # 初心者はこうして「入力集合を別の変数にコピー」して, そちらを使おう.
              # 無駄が多いが安心だ!

  if last_is_larger(b) then  # この last_is_larger の使い方が便利!!
      b.push(mean(b))
      return b
    else
      b.pop
      return b
    end

end


# ちょっと使ってみる
x = [3, 4, 8, 128, 16]
p x

y = modify_array(x)
p y
}}

さて,この例を見ると,複雑な判断を伴う問題だったが ''def'' を使うことで自然にプログラムが作れた,ということがわかる.
これは,
&br;&br;
CENTER:&size(24){''複雑な判断があるときも def で問題を段階的にわけてお手軽に!''};
&br;&br;

という感じになるだろうか(…だんだん通販番組みたいになってきた)




,を作ってみよう(各要素の二乗したもの,の平均).
そして,上のようにして,それを使ってみるサンプルプログラムを書いてみよう.


まず,わかりやすく全体構造を先に考えると,少なくとも
// programu source 表記
#highlighter(language=ruby,number=on,cache=on){{
include Math

# 半径をもらって円の面積を出力する関数.
def circle(radius)
  計算…
  return 円の面積
end

# 半径と高さをもらって円柱の体積を出力する関数.
def volume(radius,length) 
  とりあえず底面積は上の circle 関数を使って circle(radius) で求まるとして
  円柱の体積を求めたい.
  return 求めた円柱の体積
end

# 以下,プログラム本体.
r = ARGV[0].to_f
l = ARGV[1].to_f

v = volume(r,l)  # volume という関数を自分で上の方で作ったという前提で

print("Volume is ",v,"\n")
}}
という感じになるだろう.
&ref(/materials/warning.png); このように,「先に全体構造をプログラムしてしまう」と脳みそが楽をできる.だから,
CENTER:&size(24){''このように,細かいところは後回しにして,''};
CENTER:&size(24){''先にざっと全体構造を書く癖をつけるとよい.''};
&br;&br;
* 今日の総仕上げ [#ff0e3507]
&br;

&ref(/materials/warning.png); 上のプログラムでは ''#'' を使ってプログラムを読む人間用にコメントを書いている.これは,
CENTER:&size(24){''Ruby はプログラム中の ''#'' 以降 行末までを無視する''};

ことを利用している. この「コメント」はプログラムを書くとき自分の助けになるので,積極的に利用しよう.
&ref(/materials/notes.png); 
正整数 ''n'' を与えると,''n'' が素数かどうかを真偽値で返す関数をつくろう.
&br;
もちろん,たしかに正しく動くかどうか確かめておこう.

次に,「円柱の体積 = 底面積 × 高さ」であるので,これを volume 関数にプログラムすると以下のようになるだろう(該当部分だけ抜粋).
// programu source 表記
#highlighter(language=ruby,number=off,cache=on){{
def volume(radius,length) 
  return circle(radius)*length
end
}}
&br;&br;
&ref(/materials/notes.png); 
正整数 ''n'' を与えると,2以上 ''n''以下の素数の二乗和を求める関数を作ろう.
&br;
もちろん,たしかに正しく動くかどうか確かめておこう.

そして,最後に残る底面積関数 circle だが,「円の面積 = π * (半径の二乗)」なので,これをプログラミングして(該当部分だけ抜粋),
// programu source 表記
#highlighter(language=ruby,number=off,cache=on){{
def circle(radius)
  return PI * (radius ** 2)
end
}}
という感じになるだろう.
&br;

さて,これでプログラム全体が完成したので,(プログラムファイル名が cylinder.rb だとして)例えば
  ruby -w cylinder.rb 1.2 1.1

とすると
> Volume is 4.97628276328623

という出力が返ってくるはずだ.
&br;

** def のありがたみがピンと来ないんだけど? : 素数判定プログラムを書き直してみよう [#b60c94b0]

''def'' のありがたみはプログラムの機能や動きがわかりやすくなるところにあるのだが,上の例だと単純すぎて,''def'' のありがたみがわからないかもしれない(センスの有る人はわかるだろうけれども).
&ref(/materials/notes.png); そこで,前回の総仕上げである「2 から与えられた数までの整数について,すべて素数かどうか判定して出力せよ」という問題のプログラミングをこの ''def'' を使ってやり直してみよう.

まず,このプログラムに必要な機能のようなものを考えて丁寧にリストアップしてみると,

+ ある数 ''n'' がある数 ''i'' で割り切れるかどうかを知る関数
+ (上の機能を繰り返し使って)ある数 ''n'' が素数かどうか判定する関数

があれば良さそうなことは分かるとおもう.
そこで,これらを関数にすることを前提として,全体を考えてみると次のような感じになるだろう.
// programu source 表記
#highlighter(language=ruby,number=on,cache=on){{
include Math

# n と i をもらって,n が i で割り切れるかどうか出力する関数.
def divisible(n,i)
  n と i をもらって,n が i で割り切れるかどうかを判定する.
  return n が i で割り切れるなら yes, そうでなければ no という感じの返事を出力.
end

# n をもらって,n が素数かどうかを判定して出力する関数.
def is_prime(n)
  n をもらって,n が素数かどうかを判定する.
  return n が素数なら yes, そうでなければ no という感じの返事を出力.
end

# 以下,プログラム本体.
last_n = ARGV[0].to_i

for n in 2..last_n do
  if is_prime(n) then
      print(n," is a prime number.\n")
    else
      print(n," is not a prime number.\n")
  end
end
}}
&ref(/materials/warning.png); このプログラムの本体部分(15行目から21行目)をよく見よう. とてもわかりやすくなっていることに気づくだろう. これが関数を用いる利点の一つである(物事の複雑さを階層化できる).
&br;

さて,このままだともちろん動かないので,必要な関数をきちんとプログラムしよう.
まず,「n が素数かどうかを判定する」プログラムの以前のバージョンは(上にもあるが)
// programu source 表記
#highlighter(language=ruby,number=off,cache=on){{
(n が与えられているとして)
isPrime = " is a prime number."
upper = sqrt(n).floor
for i in 2..upper do
  if (n % i == 0) then
    isPrime = " is not a prime number."
  end
end 
print(n,isPrime,"\n")
}}
であったので,これを上の ''divisible'' を使う前提で ''is_prime'' 関数の文法にマッチするように直すと次のような感じになる(わかりやすいようにコメントをいれてある).
// programu source 表記
#highlighter(language=ruby,number=off,cache=on){{
# n をもらって,n が素数かどうかを判定して出力する関数.
def is_prime(n)

  # まず,n は素数だと「答え」を仮定しておく.
  answer = true

  # 割り切れるか調べる範囲は √n まででいい.
  upper = sqrt(n).floor

  # 2 から √n まで i を変えて調べる.
  for i in 2..upper do
    # n が i で割り切れるなら,「n は素数ではない」と答えを変更する.
    if divisible(n,i) then
      answer = false
    end
  end 

  # 答えを関数値として出力する.
  return answer
end
}}
&ref(/materials/warning.png); プログラム中の ''true'', ''false'' は特別な意味があって,それぞれ論理値の「真」「偽」を表す.
この真偽値は ''if'' 文の判定部分の入力値として扱える.
まあ,
CENTER:&size(24){''Ruby では真は true で,偽は false で表す''};

と覚えておけば良い.

さて,これがわかれば ''divisible'' 関数も簡単に作れる.
これまでの文法に沿って馬鹿正直に作るのならば,例えば以下のようになるだろう.
// programu source 表記
#highlighter(language=ruby,number=off,cache=on){{
# n と i をもらって,n が i で割り切れるかどうか出力する関数.
def divisible(n,i)
  if (n % i == 0) then
      return true
    else
      return false
  end
end
}}
かっこよく書くならば? 次のような感じになるだろう.
// programu source 表記
#highlighter(language=ruby,number=off,cache=on){{
# n と i をもらって,n が i で割り切れるかどうか出力する関数.
def divisible(n,i)
  return (n % i == 0)
end
}}
&ref(/materials/warning.png); なぜこう書けるか,考えてみよう.
&br;

これでプログラムは完成した.念のために全体を載せておこう.
// programu source 表記
#highlighter(language=ruby,number=on,cache=on){{
include Math

# n と i をもらって,n が i で割り切れるかどうか出力する関数.
def divisible(n,i)
  if (n % i == 0) then
      return true
    else
      return false
  end
end

# n をもらって,n が素数かどうかを判定して出力する関数.
def is_prime(n)

  # まず,n は素数だと「答え」を仮定しておく.
  answer = true

  # 割り切れるか調べる範囲は √n まででいい.
  upper = sqrt(n).floor

  # 2 から √n まで i を変えて調べる.
  for i in 2..upper do
    # n が i で割り切れるなら,「n は素数ではない」と答えを変更する.
    if divisible(n,i) then
      answer = false
    end
  end 

  # 答えを関数値として出力する.
  return answer
end

# 以下,プログラム本体.
last_n = ARGV[0].to_i

for n in 2..last_n do
  if is_prime(n) then
      print(n," is a prime number.\n")
    else
      print(n," is not a prime number.\n")
  end
end
}}
&ref(/materials/warning.png); いろいろ丁寧に書きすぎたので長くなっているが,本質としてはわかりやすくなっている.そこに着目しよう. これは「階層化して複雑さを分割することで,一度に全部を考えなくて良い」ことによる. 人間の脳みそは一度に多くの複雑さを相手にできないことを考えると,この「複雑さの分割」をしないとプログラミングはまっとうにできないということになる. つまり,
CENTER:&size(24){''def を制するものはプログラミングを制する(誇張)''};

のである.''def'' を使ってプログラムする癖を身につけよう.
&br;

** さらなる例題:コイントス [#i3809741]

''def'' の使いこなしは重要なので,他の例題もやってみよう.
&ref(/materials/notes.png); 指定した回数だけコイントスをおこなわせてコインの表と裏が何回出たか出力するプログラムを書こう.

コイントスをどうするかの具体的なことは後で詰めるとして,例によって全体をプログラミングしてしまおう.例えば次のような感じになるだろう.
// programu source 表記
#highlighter(language=ruby,number=on,cache=on){{
include Math

# コイントスを一回行って,表かどうかを出力する関数.
def is_toss_front 
  コイントスを行って,表が出るか裏が出るか調べる.
  return 表が出たら true, そうでなかったら false を出力.
end

# 以下,プログラム本体.
toss_n = ARGV[0].to_i

# 表が出た回数.最初はゼロ.
front_n = 0

# toss_n 回,コイントスを繰り返す.
for n in 1..toss_n do
  if is_toss_front then
    # 表が出たら,カウントを 1 増やす.
    front_n += 1
  end
end

# 最終結果を出力しよう.
print("front: ", front_n, " back: ", toss_n - front_n, "\n")
}}
&br;

コイントスには,Ruby のもつ乱数関数 ''rand'' を利用しよう.
これは出力が 0 以上 1 未満の乱数関数なので,
「''rand'' が 0以上 0.5未満ならばコインの表が,0.5以上 1未満ならば裏が出たと解釈すれば」
通常のコイントスをしたことに相当するはずだ.
つまり,次のような感じになる(該当部分のみ).
// programu source 表記
#highlighter(language=ruby,number=off,cache=on){{
# コイントスを一回行って,表かどうかを出力する関数.
def is_toss_front 
  if (rand < 0.5) then
      return true
    else
      return false
  end
end
}}
カッコよく書くならば? 次のように書いても良い.
// programu source 表記
#highlighter(language=ruby,number=off,cache=on){{
# コイントスを一回行って,表かどうかを出力する関数.
def is_toss_front 
  return (rand < 0.5)
end
}}
&ref(/materials/warning.png); ''rand'' は,''rand(n)'' として正整数の引数を与えると 0 以上 ''n'' 未満の整数を乱数として出力する.これを利用しても良い.余力のある人はこちらのやりかたに書き直してみよう.
&br;

さて,これでプログラム全体が完成したので,(プログラムファイル名が coin.rb だとして)例えば
  ruby -w coin.rb 50

とすると
> front: 26 back: 24

などという出力が返ってくるはずだ.ここまでやってみよう.
&br;

* 今日の総仕上げ [#zef24c00]
酔っぱらいのおっさんが歩く様子を簡単にシミュレートして出力するプログラムを作ろう.具体的には,
+ 一歩進むとき,まっすぐ前に進む確率は 40%
+ 一歩進むとき,斜め右か左に進む確率はそれぞれ 30%
+ 幅30歩分ぐらいの広い道路の真中から歩き始めた
+ (上の幅から逆算して)まあ全部で30歩分ぐらいシミュレートする
+ 一歩ごとに,どこにいるかを ''print'' 文でそれっぽく? 出力

というのが条件だ.
例えば,(プログラムファイル名が walk.rb だとして)
  ruby -w walk.rb

とすると
> ----------------o--------------
> ----------------o--------------
> ---------------o---------------
> --------------o----------------
> -------------o-----------------
> -------------o-----------------
> -------------o-----------------
> --------------o----------------
> --------------o----------------
> --------------o----------------
> ---------------o---------------
> ---------------o---------------
> ---------------o---------------
> --------------o----------------
> -------------o-----------------
> -------------o-----------------
> ------------o------------------
> ------------o------------------
> ------------o------------------
> ------------o------------------
> ------------o------------------
> ------------o------------------
> ------------o------------------
> -----------o-------------------
> ----------o--------------------
> ----------o--------------------
> ----------o--------------------
> ---------o---------------------
> ---------o---------------------
> ---------o---------------------

のような出力が返ってくるようにということである(確率の話なのでプログラムを実行する度に結果は違う).
この例だと,"o" がおっさんのいる場所を表していて,上から下へ時間が経っているということになる.
&br;

&ref(/materials/warning.png); おっさんが一歩で前か斜め右か斜め左かのどこへ進むかはコイントスと似たような関数を作れば良い.
&ref(/materials/warning.png); 場合分けが前と右と左の 3通りになるので,そこはどうする?
&ref(/materials/warning.png); おっさんの場所を ''print'' する関数も作るとわかりやすい.
&ref(/materials/warning.png); 例によってプログラム全体を先に設計して書いてしまおう.


&br;&br;
* レポート [#f5c023f9]

以下の課題について,あたうかぎり賢明な調査と考察と実行をし,

&size(18){''ExpMath1-Report-11''};

という題名をつけて e-mail にて教官にレポートとして提出せよ.なお,各自の

+ 所属(学部,学科)
+ 学籍番号
+ 学年
+ 氏名

を書くのを忘れないように.

&ref(/materials/warning.png); 自分のレポート作成ツールセットを記載せよ(もちろん,今回のレポートもそのツールセットを使って作成すること).


** レポート課題 [#x11710ce]

+ 実習で出てきた練習問題に対しての,自分の解答プログラムと,その結果を示せ.
+ 実習で出てきた練習問題等に対しての,自分の解答プログラムと,その結果を示せ.
+ 1. のプログラムを「詳細に」解説せよ.


* about Icons, ClipArts [#p8da7d98]

For details, see [[&ref(/materials/JNorth_arrow-right-sm.png); this>materials]].

// ━┃┏┓┛┗┣┳┫┻╋


// コマンドライン入力は「行頭を > で始める」.
// コマンドライン出力は「行頭をブランクで始める」.

// 実習アイコン
// &ref(/materials/notes.png);

// 注意アイコン
// &ref(/materials/warning.png);

// Link アイコン
// &ref(/materials/JNorth_arrow-right-sm.png);

// OK アイコン
// &ref(/materials/OK.png);

// NG アイコン
// &ref(/materials/NG.png);

// サンプルアイコン
// &ref(/materials/Gnome-Preferences.png);

// 大文字での強調
// CENTER:&size(24){''ほげほげ''};

// 太文字 + 赤 での強調
// &color(red){''''};

// programu source 表記
// #highlighter(language=ruby,number=on,cache=on){{}}