(090) プログラムの記述法

Ippei Kishida

Last-modified:2018/09/11 13:30:44.

条件分岐の節 においてインデント(字下げ)についてふれた。 インデントはプログラムの挙動には関係しないが、 人間がソースコードのブロック構造を視認しやすくするために行うものであった。 「とにかく動いて答えが出ているのだから、ソースコードの見栄えなんてどうでも良い」 という考え方もありうる。 初心者のうちは特にそうだろう。 しかしそれではいけない。 プログラムを自分と人の役に立てること、 そのために支払うコストを軽減することの両方のために 綺麗なコードは重要である。 諸君は他人のコードがどれくらい汚く見えるかをまだ知らない。 そして、自分が書いたものでも過去のコードがどれくらい汚く見えるかを、まだ、知らない。

1 プログラムの寿命

一般的な工業製品を考えてみよう。 製品が開発され、改善を重ねながら、普及していく。 やがて競合する新製品が登場すると競争に破れて生産が終息し、市場から消えていく。 ブラウン管テレビを例にすると、 白黒テレビが発明され、カラー化されて普及し、 やがて薄型テレビに駆逐されていった。 人の手によって作られるものは大抵このようなライフサイクルを辿る。 生産の終息した製品は、その時点で社会的な役割を終えたと言える。

プログラムも同様のライフサイクルを辿る。 プログラムが寿命を迎えるのは、 人の手によって保守されなくなった時であり、 それから誰も使わなくなるまでにかかる時間はそれほど長くはない。 Windows OS を例にして考えてみよう。 Microsoft 社が発売している Windows シリーズは 広く世界に普及している。 諸君らの目の前にあるノート PC にもインストールされている筈だ。 Windows 95 以降、98, Me, 2000, XP, Vista, 7, 8, 10 と 様々なバージョンが発売されているが、 サポートの終了したものも多数ある。 Windows XP はサポートが 2014年4月にサポート終了し、 新しいハードウェアへの対応やセキュリティパッチは提供されなくなった。 ハードウェアの新製品が次々と市場に登場していくが、 OS がそれを扱えなくなってどんどん次代遅れになり、 誰も使わないようになっていくだろう。 Windows XP は社会的な役割を終え、 Windows 10 などの新しいプログラムに世代交代していくことになる。 OS のような大きなプログラムだけではなく、 スマートフォンアプリのような小さなプログラムでも同様である。

プログラムの社会的役割とは、広く言えば自分を含めた 人の役に立つことである。 プログラムが寿命を迎えるその時までにできるだけ大きく人の役に立たせてやることが、 それを生み出した諸君らの使命である。

1.1 管理のコストの削減

この使命を果たすために重要なこと、それは管理・修正のコストを下げることである。 会社でプログラムを開発するのならば、そのコストは工数・価格に直結する。 趣味で日曜プログラミングをするのならば、そのコストは生産性と余暇の量に直結する。 では、どうすれば管理・修正のコストを下げられるか? そのための方法は無数にあるが、この授業の範囲内でできる第一のことは ソースコードを綺麗に書くことである。 ソースコードはそれを保守する未来の自分が読んで書き直すものである。 諸君は自分が書くものくらい覚えていられる筈だと甘く考えているかもしれないが、 そういう人は自分が大学に入って最初に書いたレポートが どのような構成でどのような用語を使って書いたか明瞭に思い出せるか試してみるといい。 プログラマの間には「3日前のソースは他人のソース」という言葉がある。 書き直すにはまた改めて解読する必要になるが、 誰にとっても読み易いソースコードを書くことが、 未来の自分を助けることになる。 プログラムは人間とコンピュータの仲立ちをするものである。 コンピュータはどんな汚いソースでも不平不満を言わずに読んで実行くれるが、 人間はそうではない。 汚いソースコードを相手にする事は、自分にとっても辛い作業になる。

2 汚いソースコードの清書

まず、汚いソースコードの見本を見てもらおう。

# calendar00.rb
for i in 1..12
print i, "/"
if i == 1
for j in 1..31
print j, " "
end
end
if i == 2
for j in 1..28
print j, " "
end
end
if i == 3
for j in 1..31
print j, " "
end
end
if i == 4
for j in 1..30
print j, " "
end
end
if i == 5
for j in 1..31
print j, " "
end
end
if i == 6
for j in 1..30
print j, " "
end
end
if i == 7
for j in 1..31
print j, " "
end
end
if i == 8
for j in 1..30
print j, " "
end
end
if i == 9
for j in 1..30
print j, " "
end
end
if i == 10
for j in 1..31
print j, " "
end
end
if i == 11
for j in 1..30
print j, " "
end
end
if i == 12
for j in 1..31
print j, " "
end
end
print "\n"
end

これを実行すると 以下のような結果になる。

1/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 
2/1 2 3 4 5 6 78 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 
3/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 
4/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 
5/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 
6/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 
7/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 
8/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 
9/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 
10/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 
11/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 
12/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 

このコードの意図は、各月の日を列挙することだったようだ。

2.1 インデント

このコードを綺麗にしていこう。 コードを綺麗にする作業の基本は加えた変更が結果に影響を与えないことを確認しつつ 進めることだ。 最初に 第6回において学んだインデントを入れてみよう。

# calendar01.rb
for i in 1..12
  print i, "/"
  if i == 1
    for j in 1..31
      print j, " "
    end
  end
  if i == 2
    for j in 1..28
      print j, " "
    end
  end
  if i == 3
    for j in 1..31
      print j, " "
    end
  end
  if i == 4
    for j in 1..30
      print j, " "
    end
  end
  if i == 5
    for j in 1..31
      print j, " "
    end
  end
  if i == 6
    for j in 1..30
      print j, " "
    end
  end
  if i == 7
    for j in 1..31
      print j, " "
    end
  end
  if i == 8
    for j in 1..30
      print j, " "
    end
  end
  if i == 9
    for j in 1..30
      print j, " "
    end
  end
  if i == 10
    for j in 1..31
      print j, " "
    end
  end
  if i == 11
    for j in 1..30
      print j, " "
    end
  end
  if i == 12
    for j in 1..31
      print j, " "
    end
  end
  print "\n"
end

5行ごとに1つの塊になっており、並んでいた end のそれぞれどちらが if, end に対応 するものかが分かり易くなるのが分かるだろう。

2.2 重複する部分をまとめる

コード上で 28日, 30日, 31日までになっている月ごとにまとめてみよう。

# calendar02.rb
for i in 1..12
  print i, "/"
  if i == 2
    for j in 1..28
      print j, " "
    end
  end
  if (i == 4 || i == 6 || i == 8 || i == 9 || i == 11)
    for j in 1..30
      print j, " "
    end
  end
  if (i == 1 || i == 3 || i == 5 || i == 7 || i == 10 || i == 12)
    for j in 1..31
      print j, " "
    end
  end
  print "\n"
end

大分短くなった。 長いコードはそれだけ見難い。 スクロールしなければならないのは見通せないコードを保守しなければならないのは 大変だ。 同じ内容なら短い方が書く人も読む人もラクである。

さて、このプログラムにはバグがある。 最初から気付いていただろうか? また、このバージョンで気付いただろうか? 出力をもう一度見てみよう。 8月の日付が 30 日までで終わっていた! List 1 の時点からバグが混入していたが、あのような冗長なコードでは 気付き難い。 しかし List 3 では月ごとの区別がそれぞれ1行に纏められているため、 「小の月は『にしむくさむらい』で 2, 4, 6, 9, 11 月のはず」 という常識と合わせたチェックがしやすい。 バグ修正したものが以下である。

# calendar03.rb
for i in 1..12
  print i, "/"
  if i == 2
    for j in 1..28
      print j, " "
    end
  end
  if (i == 4 || i == 6 || i == 9 || i == 11)
    for j in 1..30
      print j, " "
    end
  end
  if (i == 1 || i == 3 || i == 5 || i == 7 || i == 8 || i == 10 || i == 12)
    for j in 1..31
      print j, " "
    end
  end
  print "\n"
end

2.3 変数名を明確にする

さて、諸君はこのコードの目的を既に知っているので i, j という変数が 何を意味するのか知っている。 しかし初めて見る人がすぐに分かるだろうか? 君達が最初に List 1 のコードを見たときの気持ちを思い出して欲しい。 ちょっとコードを修正してみよう。

# calendar04.rb
for month in 1..12
  print month, "/"
  if month == 2
    for day in 1..28
      print day, " "
    end
  end
  if (month == 4 || month == 6 || month == 9 || month == 11)
    for day in 1..30
      print day, " "
    end
  end
  if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12)
    for day in 1..31
      print day, " "
    end
  end
  print "\n"
end

これならコメントがなくてもかなり分かるのではなかろうか。

2.4 コメント

コメントをつけてみよう。

# calendar05.rb
# 1 年の、月ごとの日付けを列挙する。
for month in 1..12
  print month, "/"
  # 28 日までの月
  if month == 2
    for day in 1..28
      print day, " "
    end
  end
  # 30 日までの月
  if (month == 4 || month == 6 || month == 9 || month == 11)
    for day in 1..30
      print day, " "
    end
  end
  # 31 日までの月
  if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12)
    for day in 1..31
      print day, " "
    end
  end
  print "\n"
end

2.5 空行

コメント行が上の塊につくのか、下の塊の説明なのか分かりにくい。 適宜空行を入れてみよう。

# calendar06.rb
# 1 年の、月ごとの日付けを列挙する。
for month in 1..12
  print month, "/"

  # 28 日までの月
  if month == 2
    for day in 1..28
      print day, " "
    end
  end

  # 30 日までの月
  if (month == 4 || month == 6 || month == 9 || month == 11)
    for day in 1..30
      print day, " "
    end
  end

  # 31 日までの月
  if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12)
    for day in 1..31
      print day, " "
    end
  end

  print "\n"
end

なお、空行をどの程度入れるかは趣味の問題に近く、正解はない。 入れない方が見易いという人も少なくなかろう。

2.6 メソッド(関数)を使って同様の処理をまとめる

month の条件分岐の中にある 3個の for 文はそれぞれ繰り返す回数が違うだけで ほとんど同じである。 このような「ほとんど同じ」処理をまとめるのに便利なのがメソッド 1 である。

# calendar07.rb
# 指定した日付まで列挙して出力する。
def print_days(max_date)
  for day in 1..max_date
    print day, " "
  end
end

# 1 年の、月ごとの日付けを列挙する。
for month in 1..12
  print month, "/"

  # 28 日までの月
  if month == 2
    print_days(28)
  end

  # 30 日までの月
  if (month == 4 || month == 6 || month == 9 || month == 11)
    print_days(30)
  end

  # 31 日までの月
  if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12)
    print_days(31)
  end

  print "\n"
end

メソッドは対象とする部分コードを 「def メソッドの名前」と「end」で囲んで定義する。 メソッドとなった部分は通常のプログラムのように上から順に実行されるのではなく(言い換えれば、 定義したところでは実行されない)、 その名前で呼出された時に実行される。 すなわち、定義部と実行部が分かれて別の箇所に書かれていることをしっかりと意識して欲しい。

このコードではメソッドに引数(ひきすう)を渡すようにしている。 これは数学関数 \(y = f(x)\) に対して \(x = 30\) のように値を入れるのと 似たようなイメージで捉えれば良い。 定義部の段階(l.2〜l.6)ではこの \(x\) に相当する max_date に入る実際の値が 分からないのだが、これを変数として処理を記述している。 呼出された場合には、この max_date に 30 などの値が代入されて処理される。

メソッドに関するコメント、 メソッド名などもここまでに述べた作法に則っていることに注目して欲しい。

2.6.1 プログラムの変更

上司が日付の区切り文字をカンマに変更するようにと指示してきたとしよう。 その作業にどのくらい時間がかかるだろうか。 List 1 のコードだと 12 箇所変更する必要があるのに対し、 最終的なコードでは 1 箇所で済む。

良いコードは難しいコードではなく、誰が見ても分かり易いコードである。 良いコードは行数が多くて冗長な記述の多いコードではなく、 情報密度の高いコードである。 良いコードは変更に強いものである。

2.7 まとめと補足

今一度、List 1 のコードを見てその汚さに驚いて欲しい。 綺麗なコードを書くことによって全体の見通しが良くなる。 その結果ファイル内の移動が簡単になり、 バグを発見し易くなり、 修正し易くなった。

2.7.1 変数名は明確に

まず、ダメなコードを提示しよう。

# radius00.rb
a = 1.0
a *= 2.0
a *= Math::PI
a *= 3.0
print a

諸君は既に何がダメだか指摘できる筈だ。 変数は、 多少長くなっても良いから一目で何を示す変数かが分かるような名前をつけておくべきである。 abなどの短いが意味の無い変数ばかり使っていると、 変数とその意味の対応表を用意しないと訳が分からなくなる。

また、 一つの変数はその名前の意味する使い方一つにしか使うべきではない。 言い換えれば、 変数の使い回しはしない。 radiusという名前の変数が、 プログラムの進むにつれて、 円の半径だったものが2を書けて直径になり、 \(\pi\)をかけて円周の長さになり、 最後は回転した数をかけて円が転がった距離になる、 などということは、 やってはいけない。 上のコードを改善したものが以下である。

# radius01.rb
# パラメータ設定
radius = 1.0
rotation = 3.0

# 演算
diameter = 2.0 * radius
circle = Math::PI * diameter
rotate_distance = circle * rotation

# 出力
print rotate_distance

2.7.2 コメント

他人が作成したソースコードや、 過去に自分が作成したソースコード、 数百頁に及ぶ大きなソースコードを読むことは大変である。 そのようなとき、 コメントを使って、 “面積の計算を行っている”とか“以降の部分では長さの最大値と最小値を探している。”などの説明が付記されているとソースコードを理解するときに大きな手助けとなる。 自分が1ヶ月後にプログラムをソースを少し改変したいと思ったときに、 読み直してすぐに思い出せるようなコメントが望ましい。

逆に、 全ての行にコメントがついているほどの過剰なコメントは、 本当に重要なコメントを埋もれさせてしまう。 ソースコードを見ればすぐ分かるようなことは書かなくて良い。 前項のように変数名を工夫することで無駄なコメントを減らすこともできる。 例えば三角形の面積を求める場合、 area = base * height /2.0と書いてあれば、 三角形の面積を求めているだろうことは明らかであり特にコメントは必要ないだろう。

このような観点を持てば、 コメントに書くべきは具体的な処理ではなく、 主にプログラマの意図であることが分かると思う。 何しろ処理はプログラムコード本体に正確に書かれている筈なのだ。

2.7.3 リファクタリング

コードの仕様を変更せずに内部的に整理していく作業は、 リファクタリングと呼ばれる。 リファクタリングの基本は Sec. 2.1 で軽く触れたように、 動作結果を確認・テストできる手段を用意し、 それが変化しないことを逐一確認しながら進めていくことである。

最初から最終版のようなコードを書けることが理想ではあるが、 初心者のうちは特に、そしてプロフェッショナルのプログラマでも ほとんどそうはいかない。 プログラムが初めて一応正しい結果を出力したときなどに、 適宜行うようにしたい。

3 エラーに強いプログラミング

問題が起こったときに、 簡単に止まってしまったり暴走してしまうプログラムは、 良いプログラムとは言えない。 本節では初心者起こし易いエラーの原因と、 その対処方法について簡単に述べる。

3.1 サンプル A

以下はコマンドライン引数として与えた数値の平均値を出力するプログラムである。 これがどのような時にエラーを生じるか分かるだろうか?

# average00.rb
items = ARGV
sum = 0
for i in 0..(items.size-1)
  sum += items[i].to_i
end
average = sum / items.size
print average, "\n"

3.2 サンプル B

以下はコマンドライン引数として与えた数値の平方根を出力するプログラムである。 これがどのような時にエラーを生じるか分かるだろうか?

# squareroot00.rb
print Math::sqrt(ARGV[0].to_f)

3.3 サンプル A 解説, 0割

Sec. 3.1 で述べたコードの問題点を考える。 引数を全く与えない場合を考えてみよう。 items に代入される ARGV は空配列である。 よって items.size は 0 となる。 for ループは一度も回らず、sum は 0 のままで、6 行目に入る。 6行目は sum / items.size の演算をするのだが、 これは 0/0 だ。

数学的に0での割り算は定義されていない。 プログラム上でも0での割り算はエラーとなる。 Rubyの場合は、 「divided by 0 (ZeroDivisionError)」というエラーメッセージを 出力してプログラムがストップしてしまう。 2

いきなり、 a = 1/0とする人は居ないと思うが、 c = a/bとなっているときにb = 0の状態でプログラムが実行されてしまうことはあり得る。 除算が必要になるときには分母が0になりうるかどうかを考え、 0になる場合があるのであれば対策を施す必要がある。 また計算の途中で出てくる0割にも注意が必要である。 例えば、 行列の演算において逆行列が存在しない場合、 行列式を求めるときの分母が0となる。

3.4 サンプル B 解説, 期待と異なる入力

Sec. 3.2 で述べたコードの問題点を考える。 平方根はそもそも負の値に対して実数で答えを返すことができないため、 平方根を返すメソッド Math::sqrt に負の数を与えるとエラーを生じる。

キーボードやファイルからの入力のとき、 本来数字を入力して欲しいのに文字が入力されることがある。 画面に「数字を入力してください」と表示していても、 作為不作為に関わらず文字が入力されることは多く、 この程度の対策はプログラムを作る側の責任とされることが多い。

文字とまでは行かなくても、 0が入るはずが無い場合、 負の値が入るはずが無い場合や、 あり得ないほど大きな値が入る場合などがある。

3.5 問題がないかチェックする

入力データが正常であることをチェックする条件分岐を置き、 問題ない場合のみ実行するようにする。 0割や期待と異なる入力に対して特に有効である。

# average01.rb
items = ARGV
sum = 0
for i in 0..(items.size-1)
  sum += items[i].to_i
end
if items.size != 0
  average = sum / items.size
  print average, "\n"
else
  print "配列が空のため、平均値が計算できません。\n"
end

(平方根)

# squareroot01.rb
num = ARGV[0].to_f
if num >= 0.0
  print Math::sqrt(num)
else
  print "負数のため、平方根が計算できません: ", num, "\n"
end

4 ちょっと得するノウハウ

4.1 数値は直接書かない

Sec. 2.2 で述べたように、 重複する要素をまとめることが見通し良く修正に強いプログラミングの要点である。 変数や定数を上手に利用することで行えることがある。 同じことを意味する数値が複数の箇所にあるときは、 数値をその場所に直接書かず、それより前に代入しておいた変数や定数を 用いるようにすると良い。

例えば、 重力のある系のシミュレーションをしているとき、 重力加速度9.8をいろいろなところに書いていたとする。 その後、 月での挙動をシミュレーションすることになったら、 9.8と書いてあるところを全て書き直す必要がある。 書き直し忘れることもあるし、 そもそも無駄な労力である。 その後で、 また地球でのシミュレーションをすることになったら、 また書き直すことになってしまう。 この場合では定数として、 例えばGravity = 9.8と最初に設定しておけば、 その後は定数Gravityの中身を書き換えるだけでそれを参照する全ての値を変えたことになる。 このような変数の定義はプログラムの始めの部分でまとめて行っておくと分かりやすい。

なお、 プログラムの実行中に値が変わらないものは定数としておくとよい。 定数には定数が初めて出てきたとき一度しか値を代入できず、 プログラム内で書き換えようとするとエラー(ウォーニング)になる。 これはプログラム内で間違えて書き換えることを仕組みとして防ぐ工夫である。 Rubyにおいては定数は変数の名前の先頭位置1文字を大文字にしたものである。

4.2 括弧を使う

# operator.rb
print 2 * 3 ** 2, "\n"   #(A)
print 2 * (3 ** 2), "\n" #(B)
print (2 * 3) ** 2, "\n" #(C)

上記コードの #(A) の行において、 どのような結果が出力されるか即座に分かるだろうか? 「2*3 の結果である 6 の2乗 で 36」か、 「2 に対して、 3 の2乗 の結果の 9 をかけて 18」か、 自信を持って言えるだろうか? これは演算子の優先順位の問題である。 「たし算よりもかけ算の方が先」というのは小学生レベルの常識だが、 多数ある演算子についてどれがどれより強いかを覚えることは大変だし、 大抵の場合そもそも無駄な努力である。 3 対処法は 3 つある。

いずれを用いても良いが、 括弧を使う方法が意図を明確にし、誰にとっても分かり易い良い方策であることが多い。 演算子の優先順位の間違いによって、 プログラムが意図していたのと違う動きをしてしまうことは非常に多く、 またその間違いを特定して修正するのは非常に難しい。 また、括弧を多数つけたとしてもそれが原因でエラーが起こることは、 常識的な範囲ではまずありえない。 5

ちなみに、サンプルコードの #(A) と #(B) が等価で 18 を返し、 #(C) は 36 を返す。

4.3 エラーメッセージを読む

エラーメッセージには通常、 どのようなエラーが起こったのか、 起こった箇所などの情報が含まれている。 その場所に間違いがある場合や、 そこに至る準備が足りない場合がある。 エラーメッセージが英語だからといって読まないのは、 情報をみすみす捨てていることであり、 地図を見ずに道に迷うようなものである。 エラーメッセージの文章を手がかりにWebで検索すると同じ症状に出会った人やさらにそれを解決した人の情報が得られるかもしれない。

エラーメッセージの例を以下に示す。

calendar.rb:4:in `+': String can't be coerced into Fixnum (TypeError)
        from calendar.rb:4:in `block (2 levels) in <main>'
        from calendar.rb:3:in `each'
        from calendar.rb:3:in `block in <main>'
        from calendar.rb:1:in `each'
        from calendar.rb:1:in `<main>'

「calendar.rb:4」より、このファイルの 4 行目でエラーが発覚したことが分かる。 6 「String can’t be coerced into Fixnum (TypeError)」は、 String(文字列) から Fixnum(整数) への coerce (強制型変換) ができない、 ということだ。 そのあとの from はプログラムが処理するブロックという単位ごとに、 どこでエラーが生じたかを現場に近い側から呼出し元へと遡って説明している。 少なくとも1行目だけでもキーワードを検索するなどして理解するように。 よく出るエラーの種類は限られている。

4.4 ちょっとずつ動かして確認する

Sec. 2.7.3 「リファクタリング」で述べたこととも関連するが、 初心者のうちはプログラムを一気に最後まで書いてしまうのではなく、 ある単位書き込むごとに実行して正しい動作をしているか確認しつつ進めた方が良い。 ある時点 A までは正しく動作していて、 別のある時点 B で動作がおかしいのならば、 A と B の間で編集した部分に問題がある筈である。 その間に書き加えられている量が多ければ多いほど 確認の対象が増えることになる。

4.5 コメントアウト

以下のコードはエラーが出て実行できない。

# timer00.rb
for hour in 0..23
  for minute in 0..59
    print hour, ":", minute, "\n"
    if minute == 0
    print "鳩が鳴く\n"
    #sleep 60
  end
end

実行結果は以下。

timer.rb:8: syntax error, unexpected $end, expecting keyword_end

さあ、エラーの原因がどこにあるかすぐに分かるだろうか? エラーメッセージは 8 行目でエラーが発覚したことを示しているが、 この行には「end」とあるだけでこの行に原因があるとは思えない。 高々8行くらいなら目でコードを追って見つけることも可能だが、 実際に問題になるのは 1画面におさまらないような長いコードである。 そのような長いコードで目を皿のようにして探すのはかなりやりたくない仕事だ。

そこで、あやしい部分を消してやる。 消すといっても削除するわけではく、コメント文字 # を使って Ruby の処理系から 隠してやる。

# timer01.rb
for hour in 0..23
  for minute in 0..59
#    print hour, ":", minute, "\n"
#    if minute == 0
#    print "鳩が鳴く\n"
#    #sleep 60
  end
end

なんということでしょう! エラーで止まっていたプログラムからエラーがなくなりました。 その替わりに出力すべき時刻表示も消えたが、今大事なのは「どこにエラーの原因があるか」 に関する情報である。 今コメント文字で隠した部分にエラーの原因があり、 それ以外の部分にはないことが明らかである。 あとは今マスクした行の中から探していけば良いが、 マスクした行が多ければ 半分を目安にマスクを解除していけば効率よく探すことができる。

なお、エラーの原因は「for 2 回、if 1回で合計 3個の end があるべきところに 2 個しかなかったため、 end の数が足りず対応がついていない。 なのにファイルの最後が来てしまった」 ということであった。

ここでやったように、 プログラムの一部をコメントにしてしまうことを「コメントアウト」という。 コメントアウトした部分は、 コメントアウトをすぐ元に戻すことができるところがポイントである。


  1. 言語によっては「関数」とも呼ばれる。

  2. ゼロ除算、 ゼロディバイドなどの言い方もある。

  3. プログラミング言語によって演算子の優先順序が異なることがある。 たとえば、Ruby と C では、== 演算子と & 演算子の 優先度が逆転している。

  4. Ruby と一緒にインストールされる irb コマンドが便利だろう。

  5. たとえば、1万個も括弧が入れ子になっていた場合に メモリが足りなくなってエラーになる、 ということはあるかもしれない。

  6. あくまで「発覚した」だけであって、「そこにエラーがある」とは限らない。 もっと前の行でエラーがあるかもしれない。 しかし、この行か、その前にエラーが存在することは確実である。