Ruby 簡易チュートリアル (ドラフト)
これは、Ruby の簡易なプログラミングの解説書です。サンプルはLinuxでの出力を示します。
− 目次 −
起動
Hello World
 メソッドの呼び出し
入力のエコー
 行の継続
条件文
繰り返し文
 配列とハッシュ
 ブロック
 Loop
メソッドの定義
クラスの定義
 音楽CD
 正規表現
 メソッドの追加
 ブロックの実行
ファイル
 歌詞カード
モジュール
 組込み
スレッド
 ミューテックス
例外
MySQL
日本語文字コード
落とし穴
 変数
 変数の有効範囲
 代入
 メソッドの書き換え
 インスタンス変数かメソッドか
 end end end !
関連サイト
起動
スクリプト(プログラムファイル)がある場合はこのようにします。
$ ruby hello.rb
Hello, world!
$ 
会話形式で進める場合はこのようにします。
$ irb
irb(main):001:0> puts "Hello, world!"
Hello, world!
=> nil
irb(main):002:0> exit 
$ 
Rubyはコンパイルをしません。常にインタープリタとして動作します。
Hello World
"Hello, world!"と表示するプログラムはこうなります。
puts "Hello, world!" 
Rubyは完全なオブジェクト指向言語ですが、C++やJavaのように "main()" を定義する必要は ありません。起動するとmainとして作られるスレッドの中で文が 評価されます。通常1行1文として評価されるので、Javaのように行末に セミコロンを入れる必要はありません。

"puts"は改行付き文字列を標準出力に送るメソッドです。システムなどの標準的なライブラリは 起動時に組込まれるので宣言しないで使えます。

メソッドの呼び出し

次はメソッドの呼び出しの一般形式です。
[object.]method([param[, ...]])
自分("self")で定義したメソッドやシステム標準メソッドのような 暗黙のオブジェクトが使われる時は、"object."の部分は省略できます。 メソッド名と"("の間に空白を入れると文脈によっては意味が曖昧になるので、空白は 入れないようにします。文脈によっては括弧を省略できます。
入力のエコー
端末からの入力を端末に送り返すにはこのようにします。
inp = gets
puts inp
ローカルな変数の名は英小文字かアンダースコア("_")で始めます。 変数は初めの代入文で作成されるので宣言する必要はありません。 変数自身は型を持たず、単にオブジェクトへの参照を保持するだけです。従って、 ある変数に対する操作はその時に保持しているオブジェクトのメソッドに依存します。 "gets"は標準入力から1行を改行付きで読み込み、Stringオブジェクトにして返すメソッドです。

Rubyはプログラムをワンパスで、すなわち入力の先頭から一行づつ順番に読み込んで評価します。 従って、変数には操作対象として使用する前にオブジェクトを代入しておく必要があります。 一度も代入されことのない変数を参照すると"nil"が返ります。 "nil"は特別な値で、オブジェクトの不在や論理値の 偽(false)を表します。

行の継続

一行に複数の文を記述する場合はセミコロンで区切ります。開く括弧や演算子 など文として不完全な形で行を終わると次の行に続いているものとみなされます。 行の最後にバックスラッシュを入れると常に次の行に続けられます。次はその例です。
inp = gets; puts inp
puts(
  gets
  )
puts \
  gets
Rubyの式(文)はそれ自体が値を持ちます。上の例では"gets"は読み込んだ結果の Stringオブジェクトと置き換えられます。 変数の値はそのオブジェクト、メソッドの値は戻り値、if文などの場合は最後に評価した 式の値が結果としての値となります。
条件文
次はif文を使った例です。予約語は小文字です。
print "Which city do you like to visit? Paris, London or Rome? "
city = gets.chomp.upcase
if city == "PARIS"
  puts "Visit Le Musee de Louvre."
elsif city == "LONDON"
  puts "Visit The Tower of London."
elsif city == "ROME"
  puts "Visit Colosseo."
else
  puts "Oh, you don't like to visit any of those cities."
end
"chomp"は文末の空白や改行を取り除きます。"upcase"は英文字を大文字に変換します。 "end"は、"if"を含めて様々な文で終端を示す記号として使われます。 RubyはPerlの文法に倣っているので、比較演算子はPerlに準じます。 論理演算子としては "&&" "||" "!" も "and" "or" "not" も使えますが優先度は 前者の方が上になっています。 Rubyにはさまざまな条件文があります。この例をcase文で書くとこうなります。
print "Which city do you like to visit? Paris, London or Rome? "
case gets.chomp.upcase
  when "PARIS"  then puts "Visit Le Musee de Louvre."
  when "LONDON" then puts "Visit The Tower of London."
  when "ROME"   then puts "Visit Colosseo."
  else puts "Oh, you don't like to visit any of those cities."
end
"then"を使わずに、この位置で改行しても同じことになります。 後置条件文で書くとこうなります。
print "Which city do you like to visit? Paris, London or Rome? "
city = gets.chomp.upcase
puts "Visit Le Musee de Louvre."  if city == "PARIS"
puts "Visit The Tower of London." if city == "LONDON"
puts "Visit Colosseo."            if city == "ROME"
puts "Oh, you don't like to visit any of those cities." if
  not (city == "PARIS" or city == "LONDON" or city == "ROME")
この場合、"if"部分の条件が真の時だけ対応する式が評価されます。
繰り返し文
次の例は、5から10までの数字を端末に書き出す例です。
for i in 5..10
  puts i
end
"5..10"は範囲と呼ばれるオブジェクトです。"m..n"でmからnまで(nを含む)範囲を示します。 mがnより大きい時は何もせずに終わります。 整数オブジェクトには繰り返し用メソッドとして"times"があり、無条件で特定の回数、処理を 繰り返すことができます。
3.times { puts "I love NY" }
結果はこうなります。
I love NY
I love NY
I love NY

配列とハッシュ

こちらは配列を使った繰り返しの例で、配列に納められた動物の名をひとつづつ 端末に書き出します。Rubyでは、配列要素はどんなオブジェクトでも構いません。 ただしこの例の場合、putsが文字列に変換できないオブジェクトだとエラーになります。
arr = [ "Cat", "Dog", "Rabbit" ]
arr.each do | animal |
  puts animal
end
ここで"each"は配列要素をひとつづつ取り出して"animal"に代入し、"do" から"end"までを実行します。値を受け取るための変数は"do"の直後にバー("|") で囲んで置きます。 配列要素の文字列は%wを使って次のように 見やすく書けます。
arr = %w[ Cat Dog Rabbit ]
一方ハッシュ(連想配列)は配列の添字の代わりにキーで内容を取り出します。
e_f = { "Cat" => "Chat", "Dog" => "Chien", "Rabbit" => "Lapin" }
e_f.each do | en, fr |
  puts "English: #{en} = French: #{fr}"
end
このコードは英語とフランス語の動物の名前の対応を出力します。
English: Cat = French: Chat
English: Dog = French: Chien
English: Rabbit = French: Lapin
ハッシュの場合、定義した順序と取り出される順序は必ずしも一致しません。 ダブルクォートで囲まれた文字列内の#{...}の部分はRubyが評価して結果の値で置き換えます。 中にはいかなるコードでも書けます。

ブロック

"each"メソッドは範囲や配列やハッシュに標準で組み込まれています。 従って先ほどの"for i in 5..10"の代わりに下のようにしても同じ結果になります。
(5...11).each do |i|
  puts i
end
範囲が3個のドットを持つ時は終端を含みません。つまり上の例では10までの範囲を示します。 ここで、"do"から"end"までをブロックと呼び、複数の実行文をまとめて メソッドに渡す働きがあります。 ブロックの一般形式は次のようになります。
do [| param[, ... ] |]
  statement
  [ ... ]
end
"do" "end"はそれぞれ中括弧に置き換えることができます。ブロックはまたProc オブジェクトとして変数に代入し、後で"call"を使って実行することもできます。
bl = proc { |i| puts i }
(5...11).each { |n| bl.call(n) }
この出力は次のようになります。
5
6
7
8
9
10
Rubyのこの機能は強力で、コールバックやテストやプロトタイピングなど、動的な呼び出し が欲しくなる場面で活躍します。たとえばこんなことができます。
procs = {}
procs[:plus]     = proc { |n| puts n[0] + n[1] }
procs[:minus]    = proc { |n| puts n[0] - n[1] }
procs[:multiply] = proc { |n| puts n[0] * n[1] }
procs[:divide]   = proc { |n| puts n[0] / n[1] }
operation = { :divide => [9, 2], :plus => [6, 8], :multiply => [8, 1], :minus => [9, 7] }
operation.each { |k, n| procs[k].call(n) }   # call every appropriate operation dynamically
このコードは、"operation"の内容に従って異なった計算を行い、結果を表示します。 呼ばれるコードを予め定義しておくなどの準備は必要ですが、コメントを入れた一行だけで 動的に適切なコードを呼び出せます。"#"の後ろから行末までがコメントになります。 ":plus"など、コロンを冠した要素はシンボルと呼ばれ、コロンを除いた文字の並びが そのまま値になります。

Loop

"loop"は無条件ループを作るのに使います。
loop do
  puts gets
end
このコードは強制終了させられるまで端末からの入力とエコーを繰り返します。 条件付きのループには"while"と"until"があります。
while (ans = gets.chomp.upcase) != "END"
  puts ans
end
このコードはENDと入力されるまで、入力を大文字変換してエコーし続けます。 このように条件式の中に代入文を書くこともできます。 ループの実行を制御するには、"break" "redo" "next" "retry"を使います。
count = 0
begin
  ans = gets.chomp.upcase
  break if ans == "QUIT"
  next  if ans == "SILENT"
  redo  if ans == "AGAIN"
  puts ans
end until ans == "END" or (count += 1) >= 5
"break"はループを終了します。"next"はこの後のコードを条件部の評価まで スキップします。"redo"は条件部の評価もスキップしてループの先頭から やり直します。このコードでは、SILENTを入力し続けてもエコーはしませんが カウントは数え続け、最終的には停止します。一方AGAINはエコーも せず、何回でも続けて入力できます。一方"retry"は、"for i in 0..5"のような 繰り返し文で、"for"を初め("i = 0")からやり直す場合に使います。
メソッドの定義
メインレベルのメソッドは関数のように定義して使うことができます。
def max (a, b)
  (a > b) ? a : b
end

puts max(2, 10)     ---> 10
メソッドの名前は小文字かアンダースコアで始めます。 メソッドにも引数にも型はありません。 どんなオブジェクトを渡し、何が返るのか、使う側で認識する必要があります。 メソッドは、初めて使う前に宣言する必要があります。
PREDICATE ? TRUE : FALSE
この形の式では、条件PREDICATEが真であればTRUE部、偽であればFALSE部が評価されます。 この"max"はふたつの引数のうち大きい方を返します。戻り値は最後に評価された式 の値です。コードの途中で値を持って抜けたいときは"return"文を使います。 引数の数を制限したくない場合は、引数の頭に"*"を付けると、渡された引数を配列で 受けることができます。
def max (m, *a)
  a.each { |b| m = b if b > m }
  m
end

puts max(1.732, 1_024, Math::PI/180*60)    ---> 1024
puts max("Cat", "Dog", "Rabbit")        ---> Rabbit
初めの引数だけ"m"で受けて、残りを"a"に配列で受けています。 この"max"は、全ての引数が互いに">"で比較できる限りどんなオブジェクトにも適用できます。 上の一番目の呼び出しでは整数の1024が返ります。数字の桁の間には 読み易いように任意にアンダースコア ("_")を混ぜることができます。円周率のような定数は属するクラス名と"::" で修飾して参照できます。 定数は大文字で始めます。 二番目の呼び出しでは文字の並び順で比較されて Rabbit が返ります。

逆に、扱うクラスを制限したい時はオブジェクトのクラスを調べます。 クラス名はそれ自身が定数になっています。
def min (*a)
  m = a[0]
  a.each do |n|
    return nil unless (n.is_a? Numeric)
    m = n if n < m
  end
  m
end

puts min(1.732, 1_024, Math::PI/180*60)    ---> 1.0471975511966
puts min("Cat", "Dog", "Rabbit")        ---> nil
"unless"は"if not"と同じ意味です。これは引数のクラスがNumericまたはその子孫の時だけ最小値を求めるメソッドです。
クラスの定義
クラスはオブジェクトの雛形で、その中に変数や定数やメソッドなどの定義と実行文を 含めることができます。各クラスはひとつだけ親のクラスを持つことができます。 子は親のメソッドなどを継承します。クラス名は大文字で始めます。Rubyの全てのクラスはObjectの子孫です。 下に定義の形式を示します。
class Name [< Parent]
  [statement[ ... ]]

  [attr_reader :variable[, ... ]]
  [attr_writer :variable[, ... ]]

  [def initialize[(param[, ... ])]
    [statement[ ... ]]
   end]

  [def method[(param[, ... ])]
    [statement[ ... ]]
    end]
  [ ... ]
end
クラスの実例として簡単な音楽CDを作ってみましょう。
# for a CD containing multi tracks of music
class MusicCD
  TYPE_ALBUM  = 12 # 12cm CD
  TYPE_SINGLE =  8 # 8cm CD
  @@count = 0

  attr_reader :type, :code, :title, :production, :player, :resume, :tracks
  attr_writer :resume

  def initialize(type, code, title, production, player, tracks)
    @@count += 1
    @type = type
    @code = code
    @title = title
    @production = production
    @player = player
    @tracks = tracks
    @resume = 1
  end

  def MusicCD.getCount
    @@count
  end

  def playTrack(number)
    return nil if number <= 0 or number > tracks.length
    @resume = number
    @tracks[number-1]
  end

  def resume
    @tracks[@resume-1]
  end

  def to_s
    "title=#{@title}\n production=#{@production}\n player=#{@player}"
  end
end
"attr_reader"はクラスの外部からobject.varの形でオブジェクトの属性を 読み出すための宣言です。同時に、インスタンス変数を作りますが、 その変数名はコロンを"@"で置き換えて得られます。すなわち、 インスタンス変数は頭に@をつけます。 "attr_writer"はクラスの外部からobject.var=の形でオブジェクトの属性に 書き込むための宣言です。 "initialize"はいわゆるコンストラクタで、class.newを呼び出したときに呼ばれます。

"length"は配列要素の数やStringの文字数を返すメソッドです。 "to_s"は"puts"などでオブジェクトの内容を表示する場合に使用するメソッドで Stringオブジェクトを返し、 Rubyが持つ全てのオブジェクトに慣例的に定義してあります。新しく定義するクラスにも "to_s"を持たせることにします。ダブルクォートの中の"\n"はCの"printf"のフォーマット と同じで改行(newline)文字を表します。

MusicCDには標準サイズとして12(cm)をTYPE_ALBUMという定数に、 シングルとして8をTYPE_SINGLEに持たせます。 再生を途中で止めたときに再開 するポイントを"@resume"に記憶しておくことにして、"resume"メソッドで再開する トラックの情報を返すようにします。

アットマーク二つ("@@")を冠した変数は クラス変数で、そのクラスに属す全てのインスタンスで共有されます。 ここでは、作成した音楽CDの総数を知るために、"initialize"メソッドで カウントアップすることにします。カウントの初期値はクラスを定義するときゼロにし、 現在値を取り出す"getCount"メソッドをMusicCDクラスに対して定義します。 この時の定義の仕方に注意してください。

CDの各トラックの内容を 記録するMusicというクラスも作ります。ここにはタイトル、再生時間、 フィーチャーするプレイヤーなどを納めます。
# for each music track
class Music
  attr_reader :title, :time, :feature

  def initialize(title, time, feature)
    @title = title
    @time = time
    @feature = feature
  end

  def to_s
    "title=#{@title}\n playing time=#{@time}\n featuring=#{@feature}"
  end
end
インストルメンタルな曲のほかに、 歌のためにMusicのサブクラスとしてSongを定義し、歌詞カードを納めることにします。
# for songs
class Song < Music
  def initialize(title, time, feature, lyrics)
    super(title, time, feature)
    @lyrics = lyrics
  end

  def getSongCard
    @lyrics
  end

  def to_s
    super + "\n [#{@lyrics[0]}]"
  end
end
これで、SongはMusicの定義の全てを引き継いだ上に、歌詞カードを持ったクラスになりました。 "super"は祖先のクラスで定義された同じ名前のメソッドを呼び出すメソッドです。 Songクラスの"to_s"では、親のMusicクラスの"to_s"に、歌詞カードの最初の行を 追加表示することにします。Stringオブジェクトの"+"メソッドは、ふたつの文字列を 結合します。

最後に、CDのカタログを納めるクラスを作ってみます。
# catalog of CDs
class Catalog
  attr_reader :cds
  attr_writer :cds

  def initialize
    @cds = Array.new
  end

  def to_s
    "Catalog has #{@cds.length} CDs"
  end
end
準備ができたところで、CDをいくつか作ってみます。

音楽CD

モーツアルトのホルン協奏曲はこんな感じです。
cat = Catalog.new
tracks = []
tracks << Music.new(
  "Horn Concerto in D major, K386b(412/514)", "8:11", "Alan Civil")
tracks << Music.new(
  "Horn Concerto in E flat major, K495", "16:35", "Alan Civil")
cat.cds << MusicCD.new(
  MusicCD::TYPE_ALBUM,
  "464 717-2",
  "Mozart: The Horn Concertos",
  "PHILIPS",
  "Neville Marriner/Academy of St Martin in the Fields; horn: Alan Civil",
  tracks
  )
"<<"は配列の最後に要素を追加するメソッドです。 一方、エラ・フィッツジェラルドはこんな感じです。
tracks = []
lyrics = [
  "There's a small hotel",
  "With a wishing well",
  "I wish that we were there together",
  "There's a bridal suite",
  "One room bright and neat",
  "Complete for us to share together"
  ]
tracks << Song.new(
  "THERE'S A SMALL HOTEL", "2:47", "Richard Rogers, Lorenz Hart", lyrics)
lyrics = [
  "My funny Valentine",
  "Sweet comic Valentine",
  "You make me smile with my heart"
  ]
tracks << Song.new(
  "MY FUNNY VALENTINE", "3:53", "Richard Rogers, Lorenz Hart", lyrics)
lyrics = [
  "Blue moon",
  "You saw me standing alone",
  "Without a dream in my heart",
  "Without a love of my own"
]
tracks << Song.new(
  "BLUE MOON", "3:10", "Richard Rogers, Lorenz Hart", lyrics)
cat.cds << MusicCD.new(
  MusicCD::TYPE_SINGLE,
  "POCJ-2130",
  "The Rogers And Hart Songbook Vol.2",
  "VERVE",
  "vocal: Ella Fitzgerald, conductor: Buddy Bregman",
  tracks
  )
何枚作ったか見てみましょう。
puts MusicCD.getCount        ---> 2
カタログに何枚登録されているでしょう。
puts cat
結果はこうなります。
Catalog has 2 CDs
カタログ上の全てのCDの情報は次のようにして閲覧できます。
cat.cds.each { |cd| puts cd }
title=Mozart: The Horn Concertos
 production=PHILIPS
 player=Neville Marriner/Academy of St Martin in the Fields; horn: Alan Civil
title=The Rogers And Hart Songbook Vol.2
 production=VERVE
 player=vocal: Ella Fitzgerald, conductor: Buddy Bregman
"puts"はオブジェクトを表示する際に自動的に "to_s"を呼ぶので、ここでは"puts cd"だけでうまく表示されます。

正規表現

CDをタイトルで検索するメソッドを書いてみましょう。
def findCD (cds, tag)
  cds.each do |cd|
    return cd if cd.title =~ /#{tag}/
  end
  nil
end
"=~"は正規表現との一致を取るメソッドです。正規表現は右辺でも左辺でも かまいませんがスラッシュ("/")で囲みます。 Rubyの正規表現はPerlのものにいくつかの拡張をしたものです。基本部分は次のように なっています。
. | ( ) [ \ ^ { + $ * ? これらの文字は特別な扱いを受けます。そのまま文字として一致させたい時は"\"を冠します
^行頭
$行末
.何でも1文字
\A文字列の先頭
\z文字列の末尾 (改行文字は含まない)
\b語境界 (\B はその逆)
\d数字 (\D はその逆)
\s空白。タブなどを含む。 (\S はその逆)
\w語の文字 (\W はその逆)
[^..]排除文字群 (例 [^xyz] xでもyでもzでもない文字)
c1-c2範囲 (例 0-9)
r*rが0回以上 (例 \s* 空白が0回以上)
r+rが1回以上 (例 \d+ 数字が1回以上)
r{m,n}rがm回からn回 (例 \.{1,3} ドットが1から3回)
r?rが0ないし1回
r1|r2r1またはr2 (例 (cat|dog))
(..)グループ化。部分一致を保存用変数$1, $2などに残す
"Songbook Vol.2"で検索し、見つけたCDの第2トラックの曲を調べてみましょう。
cd = findCD(cat.cds, "Songbook Vol.2")
if cd != nil
  puts cd.playTrack(2)
end
title=MY FUNNY VALENTINE
 playing time=3:53
 featuring=Richard Rogers, Lorenz Hart
 [My funny Valentine]
"findCD"は正規表現を受けてタイトルとの一致を取ります。この例では、"Vol.2"の ドットが特別な扱いを受けるので本来は"Vol\.2"のようにバックスラッシュで エスケープする必要がありますが、たまたまドットは「何でも1文字」なので、 問題なく一致が取れます。

メソッドの追加

メインでfindCDメソッドを定義する代わりにCatalogに"[]"メソッドを追加してみましょう。
class Catalog
  def [](tag)
    cds.each do |cd|
      return cd if cd.title =~ /#{tag}/
    end
    nil
  end
end
クラスの宣言に既存のクラス名やメソッド名が現れると、その名に対して追加や再定義を行います。 新しいクラスが作られるわけではありません。これでCatalogクラス自身が、登録された CDの中からタイトルを探してくれます。こんな具合です。
cd = cat["Songbook Vol\.2"]
puts cd.playTrack(3) if cd
title=BLUE MOON
 playing time=3:10
 featuring=Richard Rogers, Lorenz Hart
 [Blue moon]
Catalogに入れたCD全ての中からある曲を取り出すメソッドはこんな感じになります。
class Catalog
  def play(music)
    cds.each { |cd|
      cd.tracks.each { |track|
        return [cd, track] if track.title =~ /#{music}/i
      }
    }
    nil
  end
end
ここでの正規表現の末尾の"i"は大文字小文字を区別しないためのオプションです。 モーツアルトのホルンのための変ホ長調の曲を取り出すにはこのようにします。
music = cat.play("horn .* e flat major")
puts music if music
title=Mozart: The Horn Concertos
 production=PHILIPS
 player=Neville Marriner/Academy of St Martin in the Fields; horn:Alan Civil
title=Horn Concerto in E flat major, K495
 playing time=16:35
 featuring=Alan Civil

ブロックの実行

CDの特定の項目を調べるだけのメソッドではなく、どんな項目を探す場合でも使える ような汎用的なメソッドも書けます。
class Catalog
  def search
    list = Array.new
    cds.each { |cd| list << cd if yield(cd) }
    list
  end
end
"yield"はブロックにパラメータを渡してからブロックを評価します。 この"search"は登録された全てのCDにブロックを適用し、 ブロックの結果が真になったCDのリストを返します。たとえばシングルCDの リストを得るには次のようなブロックを渡します。
puts cat.search { |cd| cd.type == MusicCD::TYPE_SINGLE }
title=The Rogers And Hart Songbook Vol.2
 production=VERVE
 player=vocal: Ella Fitzgerald, conductor: Buddy Bregman
特定の制作会社のCDのリストは次のようにして得られます。
puts cat.search { |cd| cd.production =~ /philips/i }
title=Mozart: The Horn Concertos
 production=PHILIPS
 player=Neville Marriner/Academy of St Martin in the Fields; horn:Alan Civil
もっと複雑なたとえば歌詞に dream が現れる曲の入ったCDのリストを得ることもできます。
def lyric?(cd, tag)
  found = false
  cd.tracks.each { |track|
    if track.class == Song
      track.getSongCard.each { |line|
        if line =~ /#{tag}/i
          found = true
          break
        end
      }
    end
    break if found
  }
  found
end

puts cat.search { |cd| lyric?(cd, "dream") }
title=The Rogers And Hart Songbook Vol.2
 production=VERVE
 player=vocal: Ella Fitzgerald, conductor: Buddy Bregman
メソッド名に"!"や"?"が使えますが、慣例的に"!"は破壊的なメソッドに、 "?"は真偽値を返すメソッドの末尾に使われます。「破壊的」というのはオブジェクトの持つ属性値を 書き換えるという意味です。
ファイル
演奏する曲名のリクエストをファイルで受け取り、そのリクエストを元にカタログから曲を 引き出してみましょう。
File.open( "request.txt", "r" ) do |file|
  file.each do |line|
    music = cat.play(line.chomp)
    if not music
      puts line.chomp + " was not found"
    else
      cd, track = music
      printf( "CD: %s %s; %s, %s\n", cd.production, cd.code, cd.title, cd.player )
      printf( "Music: \"%s\", time: %s\n", track.title, track.time )
    end
  end
end
"File.open"にはファイルパスとモードを指定します。"r"は読み出し専用モードです。 "open"にブロックを指定すると終了時に自動的にファイルを閉じてくれます。 "file"の"each"はファイルの内容を1行づつ読み込んでブロックのパラメータに 代入します。 "cd, track = music"という文は、"music"配列の[0]と[1]の要素をそれぞれ"cd"と"track" に代入します。"printf"は、Cのprintfをエミュレートします。
<request.txt>
k386
small hotel
Valentine
stardust
このファイルを使って実行すると、結果はこうなります。
CD: PHILIPS 464 717-2; Mozart: The Horn Concertos, Neville Marriner/Academy of St Martin in the Fields; horn:Alan Civil
Music: "Horn Concerto in D major, K386b(412/514)", time: 8:11
CD: VERVE POCJ-2130; The Rogers And Hart Songbook Vol.2, vocal: Ella Fitzgerald, conductor: Buddy Bregman
Music: "THERE'S A SMALL HOTEL", time: 2:47
CD: VERVE POCJ-2130; The Rogers And Hart Songbook Vol.2, vocal: Ella Fitzgerald, conductor: Buddy Bregman
Music: "MY FUNNY VALENTINE", time: 3:53
stardust was not found

歌詞カード

歌詞カードをファイルに落とすには次のようにします。
song = cat.play("Small Hotel")
if song
  cd, track = song
  card = track.getSongCard
  card.collect! do |line|
    line + "\n"
  end
  File.open("smallHotel.card", "w") do |file|
    file.write(card)
  end
end
"collect!"は配列の各要素に対してブロックを適用して元の配列要素を置き換えるメソッドです。 上の例では、"card"の配列要素を書き換えます。 ここでは、各行末に改行コードを加えています。 "open"で"w"を指定すると書き込みモードになり、ファイルが存在しなければ新たに作成します。
モジュール
ここまで、演奏時間は"mm:ss"の形式で入れてきました。60分以上の場合は"hh:mm:ss" となるでしょう。これを秒で持てばどちらの曲が何秒長いとか、CDの全曲の合計演奏時間 などを簡単に計算できます。ここでは"seconds"というメソッドを作ります。 "seconds"は汎用的なのでライブラリとして外に出すことにして、timec.rb というファイルを作りましょう。 このような場合 Rubyでは"module"という機能を使います。モジュール名は大文字で始めます。
<timec.rb>
module Timec
  def Timec.seconds(time)    # time format [hh:]mm:ss
    ss, mm, hh = time.split(":").reverse
    hh = "0" unless hh
    ((hh.to_i * 60) + mm.to_i) * 60 + ss.to_i
  end
end
"split"は文字列をセパレータ文字列で分割し配列を返します。 "reverse"は配列の要素を逆に並べ替えます。"to_i"は整数に変換するメソッドです。 これを利用するプログラムは次のようになります。
require "timec"

cd, track = cat.play("My Funny Valentine")
puts Timec.seconds(track.time) if track
233
"require"は指定されたパスのファイルを読み込んで評価します。

組込み

しかしモジュールには組込みと呼ばれるさらに強力な使い方があります。Rubyには 時間に関する標準ライブラリとしてTimeクラスがあります。 現在の日付時刻を得るにはTimeクラスを使います。 しかし前述のような時間形式を変換する"seconds"メソッドはありません。
puts Time.now
puts Time.new.seconds("2:14:35")
Fri Jul 11 15:06:23 +0900 2008
undefined method `seconds' for Time:Class (NoMethodError)
しかし組込みを使うとTimeクラスにこのメソッドを簡単に追加できます。 その前にモジュール"Timec.seconds"のモジュール名を外す必要があります。 ここでは別のモジュール名で作ります。
<timex.rb>
module Timex
  def seconds(time)
    ss, mm, hh = time.split(":").reverse
    hh = "0" unless hh
    ((hh.to_i * 60) + mm.to_i) * 60 + ss.to_i
  end
end
require "timex"

class Time
  include Timex
end

puts Time.new.seconds("2:14:35")
8075
"include"は指定されたモジュールをクラスに組込みます。 Rubyには、汎用のComparableやEnumerableなどの組込みモジュールがあり、 これを自分の定義したクラスに組込むことで、比較や分類、"each"を使った処理などが 簡単にできるようになります。各曲の大小比較を演奏時間でできるようにしてみましょう。
require "timex"

class Music
  include Timex
  include Comparable

  def timeSeconds
    seconds(@time)
  end

  def <=>(other)
    seconds(@time) - seconds(other.time)
  end
end
"require"は何回出てきても同じファイル名に対して読み込むのは一度だけです。 Comparableはオブジェクトに対して比較演算子をメソッドとして定義します。 これを実際に使えるようにするには"<=>"メソッドを定義します。"<=>"メソッドは、 "a <=> b" において、正 (if a > b)、0 (if a == b)、負 (if a < b) の整数を返します。 ここでは、"seconds"を使った"<=>"を定義しています。これで、次のように 曲の長さを比較できるようになります。
cd, track1 = cat.play("My Funny Valentine")
cd, track2 = cat.play("Blue Moon")
fmt = "%s is %d seconds long\n"
printf(fmt, "My Funny Valentine", track1.timeSeconds)
printf(fmt, "Blue Moon", track2.timeSeconds)

track = (track1 >= track2) ? track1 : track2
puts track.title + " is longer"
My Funny Valentine is 233 seconds long
Blue Moon is 190 seconds long
MY FUNNY VALENTINE is longer
Songbookに納められている曲を演奏時間の短い順に並べてみましょう。
cd = cat["Songbook"]
songs = cd.tracks.sort
songs.each { |song| puts "#{song.title}, #{song.time}" }
THERE'S A SMALL HOTEL, 2:47
BLUE MOON, 3:10
MY FUNNY VALENTINE, 3:53
スレッド
Rubyのスレッドはインタプリタが管理しており、OSのネイティブなスレッド を使わないので効率は良くありません。しかしブロックを使うと読みやすく書けるので、 スレッドの使い方に慣れるには良いでしょう。MusicCDの例では、曲の演奏中に"pause"や "stop"などのコマンドで演奏を操作できるようにしてみましょう。

プレーヤとコントローラを別のスレッドにします。コントローラは、端末から コマンドを受け取りプレーヤに送ります。プレーヤはコントローラから 新しいコマンドが入るとそれに応じて"play"や"pause"を行います。プレーヤは 演奏が終了するとスレッドを終了します。コントローラは"stop"コマンドを受けると プレーヤのスレッドを終了させます。

ミューテックス

スレッド間で共有される変数はアクセスが競合しないようにミューテックスに待ち行列を 作って排他制御します。あるミューテックスがどの資源を管理するか構文上では明示されません。 資源を共有するそれぞれのスレッドが知っている必要があります。次の例では、 受け渡されるコマンドを納める変数"command"のアクセスを調停するのに使用しています。
require "thread"
mutex = Mutex.new
command = ""    # shared by threads via mutex

# player: plays the music in a thread
player = Thread.new(cat.play("K495")) do |cd|
  track = cd[1]
  time = track.timeSeconds
  button = ""
  begin
    Thread.pass
    mutex.synchronize do
      button = command
    end
    case button
    when "play"
      puts "--- playing #{track.title} remaining #{time} seconds"
      sleep(3)
      time -= 3
    when "pause"
      puts "--- pausing"
      Thread.stop
    end
  end until time <= 0
  Thread.exit
end
"Thread.new"のパラメータはそのままブロックのパラメータに渡されます。 プレーヤはコントローラからのコマンドを受ける立場なので、コマンドを 受ける前に"Thread.pass"で実行権を一旦ほかのスレッドに譲ります。 playの間は残りの演奏時間を減らして行き、pauseならスレッドを 停止(休眠状態)にします。 プレーヤは演奏が終了するとスレッドを終了("exit")します。
# controller: in the main thread, this operates the player 
begin
  input = gets.chomp.downcase
  if input =~ /^(play|pause|stop)$/
    mutex.synchronize do
      command = input
    end
    case input
    when "play"
      player.wakeup if player.status == "sleep"
    when "stop"
      Thread.kill(player)
      break
    end
  end
end until player.status == nil
player.join
コントローラは端末からコマンドを受けて、正しいコマンドならプレーヤに 送ります。playなら、スレッドがpauseで止まっていたら 動作状態にします。stopならプレーヤのスレッドを終了("kill")させ、 "player.join"でplayerのスレッドの終了を待ちます。 結果はこうなります。
play
--- playing Horn Concerto in E flat major, K495 remaining 995 seconds
--- playing Horn Concerto in E flat major, K495 remaining 992 seconds
--- playing Horn Concerto in E flat major, K495 remaining 989 seconds
pause
--- pausing
play
--- playing Horn Concerto in E flat major, K495 remaining 986 seconds
--- playing Horn Concerto in E flat major, K495 remaining 983 seconds
pause
--- pausing
stop
例外
スレッドやIOなどのメソッドを実行中にエラーが起きたときには例外を発生させて 呼び出し元に制御を返すことができます。ここでは、MusicCDでカタログにない 曲を指定された時に例外処理をしてみましょう。例外処理の基本形式は、次のように なっています。
begin
  statement[ ... 
    raise([error_type,]["error message"][,trace_back])
   ... ]
rescue [error_type]
  statement[ ... ]
[ ... ]
else
  statement[ ... ]
ensure
  statement[ ... ]
end
"begin"から初めの"rescue"までの間は通常通り評価されます。 この処理のどこかで例外(Exception)を"raise"すると、この例外を処理する "rescue"文が現れるまでスタックが巻き戻されます。 "rescue"のパラメータとしてその例外が指定されている場合は その"rescue"の中が評価されます。"rescue"に何も指定されていなければ すべての例外でその中が評価されます。複数の"rescure"文は上から 順に調べられて、合致した時点で以降の"rescue"は無視されます。 "else"の中は、例外が発生しなかった場合にだけ評価されます。 "ensure"の中は、どんな場合でも、"end"の前に評価されます。
require "thread"
mutex = Mutex.new
command = ""    # shared by threads via mutex

### exception handling
begin

  # player: plays the music in a thread
  print "Music title: "
  music = gets.chomp
  player = Thread.new(cat.play(music)) do |cd|
    Thread.main.raise RuntimeError, "'#{music}' was not found" unless cd
    track = cd[1]
    time = track.timeSeconds
    button = ""
    begin
      Thread.pass
      mutex.synchronize do
        button = command
      end
      case button
      when "play"
        puts "--- playing #{track.title} remaining #{time} seconds"
        sleep(3)
        time -= 3
      when "pause"
        puts "--- pausing"
        Thread.stop
      end
    end until time <= 0
    Thread.exit
  end

  # controller: in the main thread, this operates the player 
  begin
    input = gets.chomp.downcase
    if input =~ /^(play|pause|stop)$/
      mutex.synchronize do
        command = input
      end
      case input
      when "play"
        player.wakeup if player.status == "sleep"
      when "stop"
        Thread.kill(playing)
        break
      end
    end
  end until player.status == nil

rescue RuntimeError
  puts $!
else
  player.join
end
実行結果はこうなります。
Music title: stardust
'stardust' was not found
playerはThread.main.raiseを使って例外を上げています。 単に"raise"とすると、playerのスレッドの中だけで例外の伝播が 閉じてしまい、このスレッドに"join"をかけたときに かけた側にこの例外が渡されるようになります。 このような間接的な手段を取らずにメインのスレッドに直接例外を 上げているわけです。例外を上げたほうのスレッドは強制終了させられます。 "$!"はエラー情報を保持する特殊な変数で、"rescue"に入った時に エラーを表示するのに使っています。
MySQL
CDの情報をデータベースから取り出せると便利です。Mysqlクラスのライブラリを 使ってみましょう。Fedora(Linux)であれば「ソフトウェアの追加/削除」で ruby-mysql を検索し、インストールしてから使います。 DBから検索する最も基本的な使い方は次のようになります。
require "mysql"

mysql = Mysql.new("hostname", "username", "password", "database")
res = mysql.query("SELECT * FROM table_name")
res.each_hash do |row|
  row.each do |column, value|
    puts "#{column} = #{value}"
  end
end
mysql.close
"Mysql.new"にデータベースと接続するためのパラメータを渡します。 "query"でデータベースにアクセスして結果のオブジェクトを得ます。 "each_hash"は結果を1行ずつ連想配列で渡してブロックを評価し、 抜けるときには結果の領域を開放してくれます。 データベースの利用が終わったら"Mysql.close"で接続を切ります。 正常処理のみ示しましたが、実際には適切なエラー処理が必要です。

音楽CDのテーブルとクラスの対応は次のようにします。
クラスインスタンス変数カラムテーブル
MusicCD@typecd_typeCHAR(6)tbl_cds
@codecodeVARCHAR(30)
@titletitleTEXT
@productionproductionVARCHAR(30)
@playerplayerTEXT
Music@titletitleTEXTtbl_musics
@timetimeCHAR(8)
@featurefeatureTEXT
Song@lyricslyricTEXTtbl_songs
tbl_cdsは1CD1レコードです。 tbl_musicsは1曲1レコードでtbl_cdsへのフォーリンキーを持ち、 tbl_songsは歌詞1行1レコードでtbl_musicsへのフォーリンキーを持ちます。 データベース名はMusicCD、DBのユーザとしてmusicfanを 作成し、全てのアクセスの権利を与えます。 データベースとテーブルの定義は以下の通りです。
<データベース>
CREATE DATABASE MusicCD CHARACTER SET utf8;
CREATE USER musicfan;
GRANT ALL PRIVILEGES ON MusicCD.* TO musicfan;
<テーブル>
CREATE TABLE tbl_cds (
  id         INTEGER NOT NULL AUTO_INCREMENT,
  cd_type    CHAR(6) NOT NULL,
  code       VARCHAR(30) NOT NULL,
  title      TEXT NOT NULL,
  production VARCHAR(30) NOT NULL,
  player     TEXT NOT NULL,
  PRIMARY KEY (id)
);
CREATE TABLE tbl_musics (
  id      INTEGER NOT NULL AUTO_INCREMENT,
  cd_id   INTEGER NOT NULL,
  title   TEXT NOT NULL,
  time    CHAR(8) NOT NULL,
  feature TEXT NOT NULL,
  FOREIGN KEY (cd_id) REFERENCES tbl_cds (id),
  PRIMARY KEY (id)
);
CREATE TABLE tbl_songs (
  music_id INTEGER NOT NULL,
  lyric    TEXT NOT NULL,
  line     SMALLINT NOT NULL,
  FOREIGN KEY (music_id) REFERENCES tbl_music (id)
);
MusicCDクラスにtbl_cdsのプライマリキー(id)を持たせます。 また、メソッドを追加してtracksに曲を格納できるようにします。 CatalogクラスにもCDを格納するメソッドを追加します。 ついでに、Catalogクラスには収めたCDを順番に処理する"each" メソッドを追加します。
class MusicCD
  attr_reader :cd_id   # DB access key

  def addMusic(music)
    @tracks << music if music.is_a? Music
  end

  def set_cd_id!(id)
    @cd_id = id
    self
  end
end

class Catalog
  def addCD(cd)
    @cds << cd if cd.is_a? MusicCD
  end

  def each
    @cds.each do |cd|
      yield(cd)
    end
  end
end
"set_cd_id!"は、使い勝手がよくなるように"self"(オブジェクト自身への参照) を返すようにします。準備ができたところでデータベースにアクセスして、 先に作成したCDカタログと同じものを構成します。 データベースのエラーが起きたら処理を止めることにして、 このコード全体を"begin" "rescue" "end"の形に入れます。
begin
  # access DB and process
rescue
  # to catch an error and to do its post process
end
初めにCDの情報を読んでMusicCDオブジェクトを作成します。 トラックの情報は後で追加することにします。
catalog = Catalog.new
begin
  # access DB and process
  if not (mysql = Mysql.new("localhost", "musicfan", "", "MusicCD"))
    raise ":0: failed to connect MusicCD"
  end
  if not (res = mysql.query("SELECT * FROM tbl_cds"))
    raise ":1: failed to access tbl_cds"
  end
  res.each_hash do |r|
    type = (r["cd_type"] == "ALBUM") ? MusicCD::TYPE_ALBUM : MusicCD::TYPE_SINGLE
    catalog.addCD(MusicCD.new(type, r["code"], r["title"],
      r["production"], r["player"], []).set_cd_id!(r["id"]))
  end
"rescue"で後処理の内容を決められるように、"raise"するメッセージに処理区分(":n:")を入れました。 ":0:"であれば何もせず、":1:"なら"close"を呼ぶものとします。 DBから受け取ったデータは"each_hash"を使って1レコードづつ連想配列で処理します。 CDのタイプをデータベース上の表現からMusicCDの表現に変換してMusicCDオブジェクトを作成し カタログに収めます。
  catalog.each do |cd|
    if not (res = mysql.query("SELECT * FROM tbl_musics WHERE cd_id = #{cd.cd_id}"))
      raise ":1: failed to access tbl_musics"
    end
    res.each_hash { |r|
      if not (song = mysql.query("SELECT lyric FROM tbl_songs WHERE music_id = #{r["id"]} ORDER BY line"))
        res.free
        raise ":1: failed to access tbl_songs"
      end
      if song.num_rows > 0   # this music is a song
        lyrics = Array.new
        while (lyric = song.fetch_row)
          lyrics << lyric
        end
        music = Song.new(r["title"], r["time"], r["feature"], lyrics)
      else
        music = Music.new(r["title"], r["time"], r["feature"])
      end
      song.free
      cd.addMusic(music)
    }
  end
  mysql.close
カタログ内のCDごとに最初のSQL文でデータベ−スから複数の曲の情報を一括して取得します。 それから曲ごとに次のSQL文で複数行の歌詞を一括して取得します。歌詞があればSongオブジェクトを、 なければMusicオブジェクトを作ってCDに収めます。"fetch_row"は連想配列ではなく配列を返します。 すべての曲の処理が終われば"close"でデータベースとの接続を切ります。
rescue
  # to catch an error and to do its post process
  puts "#{$!} #{mysql.error}"
  if $! =~ /:0:/
    exit
  else
    mysql.close
  end
end
例外が発生した時にはメッセージを表示し、処理区分を見て必要ならデータベースの接続を切ります。

最後に、予めデータベースにデータを登録した上でプログラムを動作させ、 カタログが作られるかどうか表示してみましょう。
puts catalog
puts catalog.play("Blue Moon")
実行結果はこうなります。
Catalog has 2 CDs
title=The Rogers And Hart Songbook Vol.2
 production=VERVE
 player=vocal: Ella Fitzgerald, conductor: Buddy Bregman
title=BLUE MOON
 playing time=3:10
 featuring=Richard Rogers, Lorenz Hart
 [Blue moon]
日本語文字コード
使用する文字コードは"$KCODE"に値を代入して決めます。正規表現ではオプションに指定すれば 特定の文字コードとのマッチングが取れます。
グローバル変数使用する文字コード正規表現のオプション
$KCODE"EUC"EUC-Je
"SJIS"Shift JISs
"UTF8"UTF-8u
"NONE"日本語文字を使用しないn
たとえば、正規表現とのマッチングをShift JISで取るなら次のようにします。
address =~ /東京都/s
落とし穴
以上見てきたように、Rubyは暗黙の処理が多く、構文にも自由度があり、 データばかりか処理の抽象度も高くできるため、簡潔なプログラムを作れます。 しかし、Ruby特有の間違いやすさもあるので、いくつか注意点を書いておきます。

変数

変数は初めて代入されるときに作られます。Rubyの場合はコードを先読みすることが なく遂次処理されるので、プログラムの書かれた順ではなく実行する順に 文が評価されていきます。分岐がある場合などは代入が見落とされてまだ変数が 作られないうちに参照してしまうかもしれません。このようなエラーは Javaなどではコンパイル時点でわかりますが、Rubyの場合は実行時まで知ることが できません。

変数の有効範囲

変数は、ローカル(小文字か_で始まる)かインスタンス(@で始まる)か クラス(@@で始まる)かグローバル($で始まる)かの区別しかありません。 それぞれの変数は、評価される時点で既に定義されたものから探されて、 なければ作られます。 特に注意すべき点は、ブロックの引数や変数の場合、 変数が既にあればそれが使われ、なければブロック内だけの変数として 扱われることです。

代入

変数への代入は基本的にオブジェクトへの参照を代入します。 数の代入はオブジェクトのコピーが代入されるため問題になりません。 しかし通常のオブジェクトの変更は、そのオブジェクトの参照を 持った他の変数の値もまた変化してしまいます。オブジェクトを コピーするための"dup"や"clone"メソッドがありますが、 単純なオブジェクトにだけ有効です。同一のオブジェクトの参照を 複数の変数で持たないようにするか、"new"を使って新たなオブジェクトを 作るようにする必要があります。
field = %w[ Dog ]
house = field       # copy the reference to the object
field << "Fox"
puts house
puts "=========="
field = %w[ Dog ]
house = field.dup   # copy the object
field << "Fox"
puts house
Dog
Fox
==========
Dog
上は"dup"が有効ですが、下の"dup"を定義しないクラスの例ではうまく行きません。
class Animo
  def initialize
    @cage = [] 
  end

  def put(animal)
    @cage << animal
  end

  def show
    puts @cage
  end
end

field = Animo.new
field.put("Dog")
house = field       # copy the reference to the object
field.put("Fox")
house.show
puts "=========="
field = Animo.new
field.put("Dog")
house = field.dup   # copy the object, yet @cage has the reference
field.put("Fox")
house.show
Dog
Fox
==========
Dog
Fox

メソッドの書き換え

メソッドはいつでも書き換えられます。不用意に同じ名前で定義してしまうと それ以降の動作が変わってしまいます。これは意識的に使えば良い面も多いのですが、 知らないで使うと混乱の元です。特にプログラムが大きくなる場合には、 moduleを使って名前空間の管理をすべきでしょう。

パラメータの省略

メソッドをパラメータなしで呼び出すとき、括弧を省略できるので、インスタンス 変数の参照と形が同じになって区別がつきません。実際にはクラスに対する操作は メソッドの呼び出しだけなので、クラスの外からのインスタンス変数の参照は メソッドを呼び出します。しかし、括弧を省いてしまうと後に続く言語要素の どこまでがパラメータなのか曖昧になるので、時には明示的に括弧を付ける必要が あります。 メソッドの動作が期待したような結果にならない時は括弧を入れ ると良いでしょう。

end end end !

様々な構文で"end"が使われるためLISPの括弧のように"end"がたくさん並んでしまうことがあります。 こうなるとどれがどれに対応する"end"なのやら迷ってしまいます。こんな時、 ブロックのdo...endを{...}にすると区別がつきやすくなります。
関連サイト
Rubyには、これまで紹介した機能やライブラリの他にも学ぶべき多くの ものがあります。これ以降の学習は関連サイト等を参考にしてください。
END OF DOCUMENT