$ ruby hello.rb Hello, world! $会話形式で進める場合はこのようにします。
$ irb irb(main):001:0> puts "Hello, world!" Hello, world! => nil irb(main):002:0> exit $Rubyはコンパイルをしません。常にインタープリタとして動作します。
puts "Hello, world!"Rubyは完全なオブジェクト指向言語ですが、C++やJavaのように "main()" を定義する必要は ありません。起動するとmainとして作られるスレッドの中で文が 評価されます。通常1行1文として評価されるので、Javaのように行末に セミコロンを入れる必要はありません。
メソッドの呼び出し
次はメソッドの呼び出しの一般形式です。[object.]method([param[, ...]])自分("self")で定義したメソッドやシステム標準メソッドのような 暗黙のオブジェクトが使われる時は、"object."の部分は省略できます。 メソッド名と"("の間に空白を入れると文脈によっては意味が曖昧になるので、空白は 入れないようにします。文脈によっては括弧を省略できます。
inp = gets puts inpローカルな変数の名は英小文字かアンダースコア("_")で始めます。 変数は初めの代入文で作成されるので宣言する必要はありません。 変数自身は型を持たず、単にオブジェクトへの参照を保持するだけです。従って、 ある変数に対する操作はその時に保持しているオブジェクトのメソッドに依存します。 "gets"は標準入力から1行を改行付きで読み込み、Stringオブジェクトにして返すメソッドです。
行の継続
一行に複数の文を記述する場合はセミコロンで区切ります。開く括弧や演算子 など文として不完全な形で行を終わると次の行に続いているものとみなされます。 行の最後にバックスラッシュを入れると常に次の行に続けられます。次はその例です。inp = gets; puts inp
puts( gets )
puts \ getsRubyの式(文)はそれ自体が値を持ちます。上の例では"gets"は読み込んだ結果の Stringオブジェクトと置き換えられます。 変数の値はそのオブジェクト、メソッドの値は戻り値、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"部分の条件が真の時だけ対応する式が評価されます。
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 10Rubyのこの機能は強力で、コールバックやテストやプロトタイピングなど、動的な呼び出し が欲しくなる場面で活躍します。たとえばこんなことができます。
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またはその子孫の時だけ最小値を求めるメソッドです。
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を呼び出したときに呼ばれます。
# 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オブジェクトの"+"メソッドは、ふたつの文字列を 結合します。
# 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|r2 | r1またはr2 (例 (cat|dog)) |
| (..) | グループ化。部分一致を保存用変数$1, $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"を指定すると書き込みモードになり、ファイルが存在しなければ新たに作成します。
<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 longerSongbookに納められている曲を演奏時間の短い順に並べてみましょう。
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
ミューテックス
スレッド間で共有される変数はアクセスが競合しないようにミューテックスに待ち行列を 作って排他制御します。あるミューテックスがどの資源を管理するか構文上では明示されません。 資源を共有するそれぞれのスレッドが知っている必要があります。次の例では、 受け渡されるコマンドを納める変数"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
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 foundplayerはThread.main.raiseを使って例外を上げています。 単に"raise"とすると、playerのスレッドの中だけで例外の伝播が 閉じてしまい、このスレッドに"join"をかけたときに かけた側にこの例外が渡されるようになります。 このような間接的な手段を取らずにメインのスレッドに直接例外を 上げているわけです。例外を上げたほうのスレッドは強制終了させられます。 "$!"はエラー情報を保持する特殊な変数で、"rescue"に入った時に エラーを表示するのに使っています。
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"で接続を切ります。 正常処理のみ示しましたが、実際には適切なエラー処理が必要です。
| クラス | インスタンス変数 | カラム | 型 | テーブル | |
|---|---|---|---|---|---|
| MusicCD | @type | cd_type | CHAR(6) | tbl_cds | |
| @code | code | VARCHAR(30) | |||
| @title | title | TEXT | |||
| @production | production | VARCHAR(30) | |||
| @player | player | TEXT | |||
| Music | @title | title | TEXT | tbl_musics | |
| @time | time | CHAR(8) | |||
| @feature | feature | TEXT | |||
| Song | @lyrics | lyric | TEXT | tbl_songs |
<データベース> 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 | "EUC" | EUC-J | e |
| "SJIS" | Shift JIS | s | |
| "UTF8" | UTF-8 | u | |
| "NONE" | 日本語文字を使用しない | n |
address =~ /東京都/s
変数
変数は初めて代入されるときに作られます。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を{...}にすると区別がつきやすくなります。