プログラミング言語/Ruby


Rubyそぞろ歩き

 というわけでPerlの入門書"Learning Perl"(Randal L.Schwartz)、通称リャマ本から 順当にパクって行きたいと思います。

 これから小規模なプログラムを書きながらRubyの様々な機能に触れていくわけ ですが、Rubyはbeauty Perlたり得てもbeauty Perlそのものではありません。

 入門書として極めて優れたリャマ本にならい、個々の機能に関する説明は 簡単に済ませますし、Perlのソースがベースかつ複雑な機能は使わないという方針の サンプルプログラムにはかなりRuby Wayではない書き方も沢山あります。 このドキュメントに現れた書き方に決して固執しないようにしてください。

1. "Hello, world"プログラム

 さて、とりあえず何かさせてみましょう。 以下はチュートリアル文書の通過儀礼とでも言うべき例のプログラムの Ruby版です。

#!/usr/bin/ruby
print "Hello, world!\n"

 1行目は、このプログラムがRubyプログラムであることをシェルに伝えるものです。 Ruby自身にとってはコメントになっています。 多くのシェルやawk、そしてもちろんPerlと同じように、シャープ記号(#)から行末まで がコメントです。

 2行目が、このプログラム中に実行される仕事のすべてです。 あらかじめ用意されているprintという機能を呼び出して、コンソールへの 出力を成し遂げます。

 PerlやCでは、このような単純文は最後にセミコロン(;)で終わるところですが、 Rubyではそれが改行に取って代わります。もちろんセミコロンも同様に機能しますが、 Ruby Wayではありません。

 この文はBASICな道を通ってきた人には命令のように映るかもしれません。しかし print(...)のようにカッコをつけると関数のように見えるでしょう。 正解はどっちでもなくてメソッド呼び出しなのですが、この意味がわからなくても 支障はあまりありません。

2. 質問してその答えを覚えておくこと

今度は、少しだけ凝ったことをしてみましょう。hello, worldという挨拶は、 よそよそしくて他人行儀な感じがします。そこで、相手の名前を呼びかける ようにしてみましょう。これを実現するには、

 入力された名前のような、値を保持する手段として変数があります。 Rubyにはいくつかの種類がありますが、ここではローカル変数を用います。 "name"のように、素直にアルファベッドで変数を命名してください。

 次に名前の入力ですが、コンソールプログラムでは、プロンプトと呼ばれる 入力を要求するような旨の文章や記号を印字して、ユーザーからの入力を待ち受ける のが一般的です。 この仕事のうち、プロンプトの印字については前回のプログラムですでに方法を学んで いると思います。printを使うのです。あとは入力を受け付ける手段です。

 これはRuby内で標準入力を表すSTDINというモノに関わります。 Rubyにおけるモノ(=オブジェクト)には、それぞれ様々な機能(=メソッド)が備わって いて、それらを呼び出すことによってRubyのプログラムは進行します。 今回用いるのは、STDINが持つgetsというメソッドです。これは入力を一行 読み込みます。 つまり、キーボードからだとエンターキーで入力を終わるということです。

 以上をまとめると、次のようになります。

print "名前は? "
name = STDIN.gets

 この時点では、nameの値の末尾には、改行文字(\n)がくっついています(例えば Vipperと入力したら、nameの値はVipper\nになっています)。 この改行文字を取り除くには、chop!を使います。

 入力され、nameに保持された値は当然文字列なので、Ruby内ではStringという モノの種類(=クラス)から生み出されたものです。 chop!は、Stringオブジェクトが持つメソッドです。自らが表している文字列の 内容の末尾を、1字ぶん削ります*1

(以下、Stringオブジェクトが持つchop!メソッドをString#chop! のように表記することがあります。)

name.chop!

 ここまでくれば、あとはHelloと表示して、その後ろに変数nameの内容を表示する だけです。これは、ダブルクォートで囲んだ("...")文字列の中に値を埋め込む 「#{...}」という記法(式展開)を使って行います。 シェルやPerlの似たようなそれとは違い、Rubyでは式なら何でも書けます。

print "Hello, #{name}!\n"

 これらをまとめると次のプログラムが得られます。

#!/usr/bin/ruby
print "名前は? "
name = STDIN.gets
name.chop!
print "Hello, #{name}!\n"

3. 選択処理を追加する

 ここで、Vipperに対しては特別な挨拶をして、それ以外の人には普通の挨拶ですます ようにしましょう。入力された名前と文字列"Vipper"を比較して、同じであれば 特別扱いする、という処理を行います。 それらは(キーワードだけはC風の)ifとかelseとかの構文で分岐と比較を行います。

#!/usr/bin/ruby
print "名前は? "
name = STDIN.gets
name.chop!
if name == "Vipper"
  print "Hello, Vipper! よく来たな。\n"
else
  print "Hello, #{name}!\n" #普通の挨拶
end

 演算子"=="は、ここでは文字列を 比較します*2(仮に右辺が文字列ではない 場合、Rubyが出来る限り文字列に変換しようとしてくれます)。 それら文字列が等しければ(内容の比較)、結果は真になります。

 if文は、ifで始まってendで終わります。与えられた式が真ならif直後の部分を 実行し、そうでなければelseの後の部分を実行します。

4. 合言葉を当てる

 さて、名前を入力してもらったところで、プログラムを起動した人に、合言葉を 当ててもらうことにしましょう。プログラムは、Vipper以外の人に対しては、正しく 言い当てるまで、合言葉を入力するように繰り返し求めます。まずプログラムを 披露してから説明しましょう。

#!/usr/bin/ruby
secretword = "2ch"  #合言葉
print "名前は? "
name = STDIN.gets
name.chop!
if name == "Vipper"
  print "Hello, Vipper! よく来たな。\n"
else
  print "Hello, #{name}!\n" #普通の挨拶
  print "合言葉は? "
  guess = STDIN.gets
  guess.chop!
  while guess != secretword
    print "ちょwwおまwwww違うwwwww 合言葉は? "
    guess = STDIN.gets
    guess.chop!
  end
end

 まず最初に、もう一つのローカル変数secretwordに、合言葉を入れておきます。 挨拶の後に、Vipper以外の人には、(printによって)合言葉を当てるように求めます。 ユーザが当て推量した言葉を入力すると、演算子"!="によってそれを合言葉と 比較します。この場合"!="は文字列同士が違う場合に真を返します。 (この演算子の働きは"=="を用いた場合の論理的反転です) whileは式の結果によって制御される繰り返しで、式が真である限り繰り返しを 続けます。

 もちろん、これは決して安全性の高いプログラムではありません。なぜなら、 当て推量するのに飽きたユーザはCtrl+CでRubyと手を切ることができますし、 あるいはソースのsecretwordの代入式の右辺を見てカンニングすることもできる からです。 しかし、私が書いているのはセキュリティシステムではなくて、単にリャマ本の 猿真似なので、言い訳まで真似だとしても目をつぶってください。

5. 複数の合言葉を扱う

 複数の合言葉のうちどれか1つを当てればよいことにするにはどうするか―― これからそれをお話したいと思います。 これまで学んだ方法を応用するならば、ローカル変数に正解をそれぞれ入れて おいて、ユーザの当て推量と次々比較していくような手になるでしょう。 しかし、そんなやり方だと、合言葉リストを変更したり、それをファイルから 読み込んだり、曜日に応じて合言葉を変えたりするような仕組みを作ることは 困難です。

よりスマートな解決策は、配列(Array)と呼ばれるデータ構造に、 許される答えを全て入れておくというものです。 配列の要素はそれぞれがローカル変数のように値をセットしたり 取り出したりすることができます。配列全体に対して、一挙に値を与える こともできます。 ここでは、次のようにして、配列という種類(クラス)のモノ(オブジェクト)を 作り、3つの合言葉入れて、wordsというローカル変数にしておきます。

words = ["2ch", "vip", "mona"]

 Arrayオブジェクトに入れてしまえば、添え字付けによって、個々の要素に アクセスすることができます。 ですから、words[0]は2ch、words[1]はvip、words[2]はmonaになります。

 添え字(インデックス)には式を使うこともできます。例えば 変数idxに2をセットしておけば、words[idx]はmona になります。

先程の例に戻ると、次のようなプログラムになります。

#!/usr/bin/ruby
words = ["2ch", "vip", "mona"]
print "名前は? "
name = STDIN.gets
name.chop!
if name == "Vipper"
  print "Hello, Vipper! よく来たな。\n"
else
  print "Hello, #{name}!\n" #普通の挨拶
  print "合言葉は? "
  guess = STDIN.gets
  guess.chop!
  i = 0 #最初の合言葉から調べ始める
  correct = "maybe" #合言葉が当たったかどうか?
  while correct == "maybe"  #合言葉を正しく当てるまで繰り返す
    if words[i] == guess  #当たった?
      correct = "yes" #正解!
    elsif i < words.size  #他に調べる合言葉があるか?
      i = i + 1 #次回は次の合言葉を調べる
    else  #これ以上合言葉はない、だから間違い
      print "ちょwwおまwwww違うwwwww 合言葉は? "
      guess = STDIN.gets
      guess.chop!
      i = 0 #最初の合言葉からチェックをやり直す
    end
  end #「while 正しく当てるまで」の終わり
end #「if Vipper 以外」の終わり

 合言葉を調べている最中なのか、あるいはすでに見つかったかを示すのに、 変数correctを使っています。

 このプログラムは、if-else-end文のelsifブロックの使用例にもなっています。 Cやawkには、これに相当するものは存在しません。 elsifブロックは、elseブロックと新しいif条件を合わせたもの省略形ですが、 もう一組のif-endブロックをネストしないで済みます。

 if-elsif-elsif-elseif-elseの連鎖によって、一連の条件を比較するというのは、 とてもPerlらしいやり方らしいです。Rubyではそうでもないです((Rubyには case文という、Cのswicth文を幾分高機能にしたような構文があります))。

6. 1人1人に別の合言葉を割り当てる

 たった今作ったブログラムでは、通りすがりの人間でも、3つの言葉のどれかを 当てれば、成功してしまいます。もし1人1人に別の合言葉を割り当てたいのなら、 人と合言葉を対応づけるテーブルが必要になります。

合言葉
Hiroyukiumai-bou
Boomfugashi
Yaruoonani

 このようなテーブルを表現するのに最も適した方法は、連想配列という データ構造を使うことです。RubyではHashクラスから作ることができます。

Hashオブジェクトの各要素は、普通の配列と同じように、1個1個が値を 持っています。ただ、Hashの個々の要素にアクセスするには インデックスではなくキー(key)を使います。このキーには、Rubyの値 *3ならなんでも用いることができます。

上のようなテーブルを表現するのに、人名を表すStringオフジェクトを キーにして、合言葉のStringオブジェクトを格納するには次のように します。

words = {
  "Hiroyuki" => "umai-bou",
  "Boom"     => "fugashi",
  "Yaruo"    => "onani",
}

 カンマで区切られたリストのそれぞれの、「=>」で区切られた左右の値が、 Hashのキー1個とそれに対応するオブジェクトを表しています。 この代入文が、継続文字のたぐいを使わずに、数行にまたがっていることに 注目しましょう。このような書き方ができるのは、Rubyプログラムにおいては、 一般の空白文字は意味を持たないからです。

 Boomの合言葉を取り出すには、Boomをキーとして、 words["Boom"]といった式によってHashオブジェクトwordsの要素に アクセスする必要があります。先程の配列と同じように、この参照によって 得られる値はfugashiになります。

 また、普通の配列(Array)と同様に、キーには任意の式を用いることができます。 personに"Boom"をセットしてから、words[person]を評価すると、 同様にfugashiが得られます。

 これらを1つにまとめると次のプログラムが得られます。

#!/usr/bin/ruby
words = {
  "Hiroyuki" => "umai-bou",
  "Boom"     => "fugashi",
  "Yaruo"    => "onani",
}
print "名前は? "
name = STDIN.gets
name.chop!
if name == "Vipper"
  print "Hello, Vipper! よく来たな。\n"
else
  print "Hello, #{name}!\n" #普通の挨拶
  secretword = words[name]  #合言葉を得る
  print "合言葉は? "
  guess = STDIN.gets
  guess.chop!
  while guess != secretword
    print "ちょwwおまwwww違うwwwww 合言葉は? "
    guess = STDIN.gets
    guess.chop!
  end
end

 合言葉を調べる部分に注目しましょう。合言葉が見つからなければ、 secretwordの値はnilという値になります。なのでnilであるかを チェックすれば、その他全員に対するデフォルトの合言葉を設定することも できます。この処理は次のようになります。

[... プログラムの前の部分を省略 ...]
  secretword = words[name]  #合言葉を得る
  if secretword == nil  #おや、見つからない
    secretword = "fushianasan"  #いいかお前ら、絶対に名前欄には入れるなよ! 絶対だぞ!
  end
  print "合言葉は? "
[... プログラムの残りの部分を省略 ...]

7. いろいろな書き方を受け付けるようにする

 もし、ユーザがVipperの代わりにvipperやVipper Yaruoなどとタイプしたら、 その他大勢として扱われてしまうでしょう。なぜなら、==による比較は、完全に 一致しているかどうかを調べるからです。 これをうまく扱う方法の1つを紹介しましょう。

 Vipperそのものの代わりに、Vipperで始まる任意の文字列を探す、ということで 話を進めましょう。sedやawkやgrepでは、正規表現(regular expression)を利用して、 これを実現することができます。 正規表現とは、マッチする(一致する)文字列の集合を定義するテンプレートです。 Vipperで始まる任意の文字列にマッチするRubyの正規表現は、sedやawkやgrepと 同様に、^Vipperと なります*4

 この正規表現が、nameに入っている文字列とマッチするかを調べるには、 マッチ用の演算子を使って次のようにします:

if name =~ /^Vipper/
  ## はい、マッチしました
else
  ## いいえ、マッチしませんでした
end

 正規表現の前後をスラッシュで囲むことに注意しましょう。スラッシュで囲まれた 部分では、文字列の場合と同様に、スペースやその他の空白文字は意味を持ちます。

 これでほぼ完璧なのですが、あとvipperを受け付けることと、 Vipperlなどをはねのけることが残されています。vipperを受け付けるには、右側の スラッシュの直後に、大文字と小文字を区別しないことを意味するオプションiを 置きます。 Vipperlをはねのけるには、(viやいくつかのバージョンのgrepと 同様な)単語の境界を表す特別なマーカー\bを追加します。 これによって、正規表現中のrの直後には、英文字がこないことを保証します。 結局、正規表現は/^vipper\b/iとなりますが、これは「文字列の 先頭*5にvipperがあり、その直後に英文字や数字が きてはならない。また、大文字小文字のどちらでもOK」という意味になります。

 これをプログラムの残りの部分と合わせると、次のようになります:

#!/usr/bin/ruby
words = {
  "Hiroyuki" => "umai-bou",
  "Boom"     => "fugashi",
  "Yaruo"    => "onani",
}
print "名前は? "
name = STDIN.gets
name.chop!
if name =~ /^vipper\b/i
  print "Hello, Vipper! よく来たな。\n"
else
  print "Hello, #{name}!\n" #普通の挨拶
  secretword = words[name]  #合言葉を得る
  if secretword == nil  #おや、見つからない
    secretword = "fushianasan"  #いいかお前ら、絶対に名前欄には入れるなよ! 絶対だぞ!
  end
  print "合言葉は? "
  guess = STDIN.gets
  guess.chop!
  while guess != secretword
    print "ちょwwおまwwww違うwwwww 合言葉は? "
    guess = STDIN.gets
    guess.chop!
  end
end

 ご覧のように、このプログラムを、最初のちっぽけなhello worldプログラムと 比べると、「よくぞここまで成長したものだ」という感慨にとらわれます。それでも、 このプログラムは、まだまだ十分に小さく取り扱いも楽な上に、小さい割には なかなかの働き者です。これがRuby流のプログラミングなのです。

 Perlは、UNIXのあらゆる標準ユーティリティ(非標準のものもいくつか含みます)が 持っている、あらゆる正規表現の機能を提供しています。それだけではありません。 Perlの文字列マッチはこの惑星上でほぼ最高速なので、性能は犠牲になりません。

 …らしいですけど、Rubyではどうなんでしょうね。 1.8系までのRubyの正規表現エンジンは、Emacsでの実装を改良してPerl5互換にしたり したとかいうややこしい歴史を経ていてだいぶブラックボックスらしいです。 もっと早くしてよーとねだっても難しいかも。

 とりあえずPerl5互換なので機能は十分でしょう。

8. その他大勢も公平に扱う

 さて、これでVipperやvipperやVipper Yaruoなどと入力しても大丈夫になりました が、Vipper以外のユーザに関してはどうでしょうか? Yaruoは相変わらずyaruoと タイプしなければなりません(yaruoの後ろにスペース1つ置くことも許されません)。

 yaruoに対しても公平であるためには、テーブルを引きにいく前に、入力された 行の最初の単語を取り出して小文字にする処理が必要です。これには二種類の メソッドを用います。 String#subは正規表現にマッチするものを探して、それを与えられた文字列で 置き換えます。 String#downcaseは、文字列を小文字にまとめるのに使います。

 まずは、String#subです。私たちがしたいのは、nameの内容を調べて、単語を 構成しない最初の文字を探して、そこから文字列の末尾までを消してしまうことです。 このために必要な正規表現は/\W.*/です―――\Wは単語を構成しない文字 (nonword charactor: 英文字、数字、下線以外のもの)を表し、.*はそこから 行末までの文字すべてを表します。さて、マッチした文字を削除するには、次のように して、文字列のうち正規表現にマッチした部分を空文字列で置き換えます。

name = name.sub(/\W.*/, "")

 第一引数にマッチさせる正規表現、第二引数に置き換える文字列を置くのが String#subの基本的な使い方です。

 String#downcaseは名前通り、文字列をまとめて小文字にして 返します*6

 これらを、残りの部分とドッキングすると、次のプログラムになります:

#!/usr/bin/ruby
words = {
  "hiroyuki" => "umai-bou",
  "boom"     => "fugashi",
  "yaruo"    => "onani",
}
print "名前は? "
name = STDIN.gets
name.chop!
original_name = name  #挨拶で使うためにとっておく
name = name.sub(/\W.*/, "") #最初の単語より後ろの部分をすべて消す
name = name.downcase  #全てを小文字にしてしまう
if name == "vipper" #今度はこの比較のやり方でおk
  print "Hello, Vipper! よく来たな。\n"
else
  print "Hello, #{original_name}!\n"  #普通の挨拶
  secretword = words[name]  #合言葉を得る
  if secretword == nil  #おや、見つからない
    secretword = "fushianasan"  #いいかお前ら、絶対に名前欄には入れるなよ! 絶対だぞ!
  end
  print "合言葉は? "
  guess = STDIN.gets
  guess.chop!
  while guess != secretword
    print "ちょwwおまwwww違うwwwww 合言葉は? "
    guess = STDIN.gets
    guess.chop!
  end
end

 前のバージョンでは、Vipperをチェックするのに正規表現を使っていましたが、 ここでは以前使ったような単純な比較に逆戻りしています。なぜかといえば、 新しく追加した置換と変換処理によって、vipper yaruoもVipperも、結局はvipperに なってしまうからです。 そしてBoomやBoom kunはともにboomになり、HiroyukiやHiroyuki, created 2chは hiroyukiになる、というようにVipper以外の人たちも公平な扱いを受けるように なります。

 文を2、3個追加するだけで、プログラムはずいぶんユーザフレンドリになりました。 ちょっとキーを叩くだけで、高度な文字列操作を表現できるのが、Rubyの数ある 利点の一つなのです。…と、LLが流行ってきた昨今となってはあんまり威張れない でしょうか。

 しかし、比較したりテーブルを調べたりするために名前を加工してしまうと、 入力された名前そのものは壊されてしまいます。そこで、加工する前に、名前を original_nameに保存しておきます。(C言語の識別子と同様に、Rubyの変数名は 英文字、数字、下線から構成され、長さはほぼ無制限です。) こうしておけば、後ほど必要なときにoriginal_nameを参照することができます。

 Rubyには、文字列を調べたり、いじったりする手段がたくさん用意されています。 Stringクラスのメソッドはもちろん、Regexp(正規表現)オブジェクトも 関わってきますし、標準添付ライブラリにStringIOやStringScannerといったものも あります。

9. もう少しモジュール性を高める

作成中…


*1 1字、というのは正確ではなく、実際は1バイト
*2 左辺のオブジェクトが動作を決めてます
*3 つまりオブジェクト
*4 ほんとはRuby的には\AVipperのほうがジャスティス
*5 ほんとは「行の先頭」
*6 String#trメソッドというものもあるのですが、マイナーなので

トップ   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS