今まで見てきたように、ループやイテレータを使えば、同じことを (同じコードを)何度も何度も実行させることが出来ます。 しかし、時には同じ事を何度も行いたいけれどプログラムの中で違う場所から それを行いたいこともあるでしょう。 たとえば、心理学の学生のためにアンケートプログラムを書いてるとしましょう。 知り合いの心理学コースの学生たちからもらったアンケートの内容は 次のようなものでした。
puts 'こんにちは, 私の実験のためにお時間をとって' puts 'いただきありがとうございます. この実験は, ' puts '人々がメキシコ料理に対してどう感じているか' puts 'に関するものです. メキシコ料理について考え' puts 'すべての質問に対して, yes か no かで正直に' puts '答えてください. この実験はおねしょとは関係' puts 'ありません.' # いくつかの質問をしますが、答えは無視してしまいます。 goodAnswer = false while (not goodAnswer) puts 'タコスは好きですか?' # タコス《トルティーヤに肉・ answer = gets.chomp.downcase # チーズ・野菜などをくるんだ if (answer == 'yes' or answer == 'no') # メキシコ料理》 goodAnswer = true else puts '"yes" か "no" かでお答えください.' end end goodAnswer = false while (not goodAnswer) puts 'ブリートは好きですか?' # ブリート《肉・チーズなどを answer = gets.chomp.downcase # トルティーヤで包んで焼いた if (answer == 'yes' or answer == 'no') # メキシコの料理》 goodAnswer = true else puts '"yes" か "no" かでお答えください.' end end # しかし、「この」回答には注目します。 goodAnswer = false while (not goodAnswer) puts 'ベッドを濡らしますか?' answer = gets.chomp.downcase if (answer == 'yes' or answer == 'no') goodAnswer = true if answer == 'yes' wetsBed = true else wetsBed = false end else puts '"yes" か "no" かでお答えください.' end end goodAnswer = false while (not goodAnswer) puts 'チミチャンガは好きですか?' # チミチャンガ《スパイスを効かせた answer = gets.chomp.downcase # 肉などをトルティーヤで包んで if (answer == 'yes' or answer == 'no') # 揚げたメキシコ料理》 goodAnswer = true else puts '"yes" か "no" かでお答えください.' end end puts 'さらにいくつか質問を重ねます...' goodAnswer = false while (not goodAnswer) puts 'ソパイピーヤは好きですか?' # ソパイピーヤ《四角い形をした answer = gets.chomp.downcase # 練り粉の揚げ物;蜜をかけたり if (answer == 'yes' or answer == 'no') # してデザートにする》 goodAnswer = true else puts '"yes" か "no" かでお答えください.' end end # メキシコ料理に関して他にもたくさんの質問をします。 puts puts 'デブリーフィング' #(訳註: 実験後、被験者にその目的・理由を明かすこと) puts 'この実験へのご協力感謝します. 実は, この実' puts '験はメキシコ料理とは何の関係もありません. ' puts 'この実験は夜尿症(おねしょ)に関する実験だっ' puts 'たのです。メキシコ料理は、より正直に答えら' puts 'れるようあなたのガードをはずすために聞いた' puts 'に過ぎません。本当にありがとうございました.' puts puts wetsBed
こんにちは, 私の実験のためにお時間をとって いただきありがとうございます. この実験は, 人々がメキシコ料理に対してどう感じているか に関するものです. メキシコ料理について考え すべての質問に対して, yes か no かで正直に 答えてください. この実験はおねしょとは関係 ありません. タコスは好きですか? yes ブリートは好きですか? yes ベッドを濡らしますか? no way! "yes" か "no" かでお答えください. ベッドを濡らしますか? NO チミチャンガは好きですか? yes さらにいくつか質問を重ねます... ソパイピーヤは好きですか? yes デブリーフィング この実験へのご協力感謝します. 実は, この実 験はメキシコ料理とは何の関係もありません. この実験は夜尿症(おねしょ)に関する実験だっ たのです。メキシコ料理は、より正直に答えら れるようあなたのガードをはずすために聞いた に過ぎません。本当にありがとうございました. false
これは繰り返しのたくさん入った非常に長いプログラムです。 (メキシコ料理に関する質問のところのコードはみな同じ物で、夜尿症 の質問が少し異なるだけです。) コードの繰り返しは悪いことです。たとえ、質問と質問の間に何か させたいことがあったりして、その繰り返しを大きなループや イテレータに組み込むことができなかったとしてもです。 こういった場合、メソッドを書くのがベストです。 まずは簡単な例でどうするのか示しましょう。
def sayMoo # (訳註)'moo'は、牛の鳴き声 puts 'モォーーー ' end
んー。このプログラムはsayMooを実行しません。 なぜでしょう。それは、実行するように告げなかったからです。 ここに書いてあるのは、どうやって sayMoo するかであって、実際にしろ と命令しているわけでは ないのです。ではこれにもう少し追加します。
def sayMoo puts 'モォーーー ' end sayMoo sayMoo puts 'コイン-コイン' sayMoo sayMoo
モォーーー モォーーー コイン-コイン モォーーー モォーーー
これで、実行できましたね。(フランス語を話さない人のために説明 しておきますと、プログラムの中央で鳴いているのはフランスの アヒルです。フランスではアヒルは"コイン-コイン" と鳴きます。)
こうして、sayMooというメソッドを 定義(define) することができました。(メソッド名は、ほとんどの場合、 変数名と同じように小文字のアルファベットで始まります。 +や==と言ったわずかな例外もありますが。) しかし、メソッドは常にオブジェクトに付随していなければ ならないものだったはずです。実は、こうして定義した場合、 (putsやgetsのときと同様に)、メソッドは プログラム全体というオブジェクトに付随するようになります。 次の章で、プログラム全体ではなく、特定のオブジェクトに付随する メソッドを定義する方法を覚えます。 でもその前にまずは覚えなければならないことがあります。
すでに気が付いていると思いますが、メソッドには(gets, to_sやreverseなど)、ひとつのオブジェクトに 対して単独で用いられるものと、(+や-、あるいは putsなどのように)、引数(ひきすう) (parameter) を取って、オブジェクトがそのメソッドをどのように実行するかを指示する 必要があるものがあります。 たとえば、5+とだけ言っても意味が通じませんね。 これは、5に対して加算するということを告げていますが、 何を 足すのかということは告げていません。
sayMooの定義に引数を付け加えるには(たとえば、モーと鳴く 回数を付け加えてみましょう。)、こんなふうにやります。
def sayMoo numberOfMoos puts 'モォーーー ' * numberOfMoos end sayMoo 3 puts 'オインク-オインク' sayMoo # この行は引数がないためエラーが出てしまいます。
モォーーー モォーーー モォーーー オインク-オインク #<ArgumentError: wrong number of arguments (0 for 1)>
numberOfMoosというのは、与えられた引数を指し示す変数です。 ちょっと紛らわしいので、もう一度言いましょう。 numberOfMoosは、(メソッドを実行するときに)与えられた引数を (メソッドの中から)指し示すための変数です。 もし、sayMoo 3 というコードがあればその引数は3です。 そして、変数 numberOfMoos は3を指し示します。
さて、こうしてこのプログラムではこのように引数が 必要 となりました。 では、もし、引数をつけなかったらsayMooは 'モォーーー 'に対して何を掛けるのでしょう。 コンピュータにはどうにもなりません。
もし、Rubyのオブジェクトが英語の名詞で、メソッドが動詞に相当するのであれば、 引数は副詞(たとえば、sayMooでは、引数は、どのように sayMooするかを指示)か、または、 直接目的語(たとえば、putsでは、引数は何を puts するのかを指示)に当たるとみなせます。
以下のプログラムでは、2つの変数があります。
def doubleThis num numTimes2 = num * 2 puts num.to_s+' の2倍は '+numTimes2.to_s end doubleThis 44
44 の2倍は 88
その2つの変数とはnumとnumTimes2です。 そのどちらもdoubleThisというメソッドの中にあります。 このような変数は(そして、ここまでで出てきた変数全部がそうなのですが) 局所変数 です。この意味は、これらの変数が メソッドの内部にのみ存在できて、そこから出ることができない、 ということです。実際やってみるとエラーが返ってきます。
def doubleThis num numTimes2 = num * 2 puts num.to_s+' の2倍は '+numTimes2.to_s end doubleThis 44 puts numTimes2.to_s
44 の2倍は 88 #<NameError: undefined local variable or method `numTimes2' for #<StringIO:0x402b02ec>>
Undefined local variable... そんな局所変数は定義されていませんよ、ということですね。 numTimes2という局所変数はメソッドの中で定義した はずなのですが、その変数を使おうとしている所が「局所」では なかった、ということのようです。 局所というのはメソッドの中のみという意味です。
このことは不便なように見えますが、さにあらず。 実際には非常にうまい機能なのです。 メソッドの中の変数が外からアクセスできないということは、 メソッドの外側であなたが定義した 変数も、メソッドの中から アクセスできない、つまり、消されたりすることがないということです。
def littlePest var var = nil puts 'ハハ! お前の変数は破壊したぜ!' end var = '君はこの変数に手を触れることはできない!' littlePest var puts var
ハハ! お前の変数は破壊したぜ! 君はこの変数に手を触れることはできない!
この小さなプログラムの中には実際にはvarという名前の 2つの 変数があります。ひとつはlittlePestの内側、 もうひとつは外側です。で、littlePest varというふうに メソッドを呼ぶと、文字列がひとつのvarからもうひとつの varへと渡されます。その結果、どちらもが同じ文字列を指し示す ようになります。そのあと、littlePestがそれ自身の局所 変数であるvarの指し示す先をnilに替えます。 しかし、このときメソッドの外にあるvarは何もいじられることはありません。
メソッドの中には、呼ばれたとき何か値を返すものがあることに 気が付いていると思います。たとえば、getsは (キーボードから入力された)文字列を返し ますし、 5+3(これは、実は5.+(3)のことなんですが) の中の+というメソッドは8を返します。 数に対する算術メソッドは数を返しますし、文字列に対する 算術メソッドは文字列を返します。
メソッドが呼ばれたところへ値を返すのと、プログラムが画面に対して 出力するの(putsの動作)との違いを理解しておくことは重要です。 5+3は8を返しますが、8を出力する わけではありません。
それでは、putsは何を返し ているのでしょう? 今までは気にしてきませんでしたが、ここで見てみることにしましょう。
returnVal = puts 'この puts が返しているのは:'
puts returnVal
この puts が返しているのは: nil
この結果、最初のputsはnilを返していることがわかります。 ここではチェックしていませんが、2つ目のputsも同様です。 つまり、putsは常にnilを返します。 すべてのメソッドは、何かを返さなければなりません。たとえそれがnilで あってもです。
ではここでちょっと読むのを中断して、 先ほどのプログラムでsayMooが何を返しているのか 調べるプログラムを書いてみてください。
びっくりしましたか? どうなったのか説明しましょう。 メソッドからの返り値は単純にメソッドの最後の行の値です。 sayMooの場合、これから行くと puts 'モォーーー '*numberOfMoosが返ることになり、すなわち nilが返ります。なぜならputsはいつもnilを 返すからです。 もし、このメソッドが'黄色い潜水艦'という文字列を返すように したければ、それを メソッドの終わりに追加するだけでよいわけです。 つまり、
def sayMoo numberOfMoos puts 'モォーーー '*numberOfMoos '黄色い潜水艦' end x = sayMoo 2 puts x
モォーーー モォーーー 黄色い潜水艦
さて、それではようやく最初の心理学の実験に戻ってみましょう。 今度は質問の繰り返しのために、自分で定義するメソッドを使います。 質問は引数を取って、yesと答えたときにはtrueを noと答えたときにはfalseと答えるようにしましょう。 (たとえ、大部分の答えを無視する場合でも、答えを返すというのは良いアイデアです。 このやり方は夜尿症の質問でも使えるわけですから。) それと、今回は挨拶とデブリーフィングのところは省略します。 これで少しは読みやすくなるでしょう。
def ask question goodAnswer = false while (not goodAnswer) puts question reply = gets.chomp.downcase if (reply == 'yes' or reply == 'no') goodAnswer = true if reply == 'yes' answer = true else answer = false end else puts '"yes" か "no" かでお答えください.' end end answer # これが返す値(true または false)です。 end puts 'こんにちは, 私の実験のために....' puts ask 'タコスは好きですか?' # この返り値は無視します。 ask 'ブリートは好きですか?' wetsBed = ask 'ベッドを濡らしますか?' # この返り値は保存します。 ask 'チミチャンガは好きですか?' ask 'ソパイピーヤは好きですか?' ask 'タマーレは好きですか?' puts 'さらにいくつか質問を重ねます...' ask 'オルチャタを飲むのは好きですか?' ask 'フラウタスは好きですか?' puts puts 'デブリーフィング:' puts 'ご協力感謝します...' puts puts wetsBed
こんにちは, 私の実験のために.... タコスは好きですか? yes ブリートは好きですか? yes ベッドを濡らしますか? no way! "yes" か "no" かでお答えください. ベッドを濡らしますか? NO チミチャンガは好きですか? yes ソパイピーヤは好きですか? yes タマーレは好きですか? yes さらにいくつか質問を重ねます... オルチャタを飲むのは好きですか? yes フラウタスは好きですか? yes デブリーフィング: ご協力感謝します... false
どうです、悪くないですねぇ。(訳註:同じコードの繰り返しがだいぶ減りました。) 質問を追加するのも簡単 です。追加したとしても、プログラムは小さい ままです。これは大きな進歩— 怠け者プログラマにとっては夢のようです。
ここで、もうひとつのメソッドの例を出しましょう。 englishNumberです。 これは、たとえば22のようなひとつの数をもらって、 その英語読みの文字列(この場合'twenty-two'のような)を返します。 とりあえず、0から100までの整数についてだけ 働くようにしましょう。
(注意: このメソッドは、 さっさとメソッドから返る return というキーワードと、新しい種類の分岐法 elsif を使っています。これらがどんなふうに働くのかはプログラムの文脈から 明らかでしょう。)
def englishNumber number # 0から100だけの数を受け取るようにします。 if number < 0 return 'ゼロ以上の数を入力してください.' end if number > 100 return '100以下の数を入力してください.' end numString = '' # これが最終的に返す文字列です。 # 上のほうの桁から表示させていって、"left" には何の数がまだ残っているかを入れます。 # "write" にはこれからすぐに出力させるための一桁分の数を入れます。 # write と left... いいですか? :) left = number write = left/100 # 百の桁をまず書き出しましょう。 left = left - write*100 # leftからは百の桁を取り除きます。 if write > 0 return 'one hundred' end write = left/10 # 今度は十の桁です。 left = left - write*10 # そして十の桁を引きます。 if write > 0 if write == 1 # 参りますねぇ。 # 英語では12は"twelve"であって、"tenty-two"と言うわけには行かないので、 # 十の桁が1のときは特別扱いしなければなりません。 if left == 0 numString = numString + 'ten' elsif left == 1 numString = numString + 'eleven' elsif left == 2 numString = numString + 'twelve' elsif left == 3 numString = numString + 'thirteen' elsif left == 4 numString = numString + 'fourteen' elsif left == 5 numString = numString + 'fifteen' elsif left == 6 numString = numString + 'sixteen' elsif left == 7 numString = numString + 'seventeen' elsif left == 8 numString = numString + 'eighteen' elsif left == 9 numString = numString + 'nineteen' end # writeが1の場合、一の桁に関してはもう処理してしまっていますので、 # もう出力する必要はありません。 left = 0 elsif write == 2 numString = numString + 'twenty' elsif write == 3 numString = numString + 'thirty' elsif write == 4 numString = numString + 'forty' elsif write == 5 numString = numString + 'fifty' elsif write == 6 numString = numString + 'sixty' elsif write == 7 numString = numString + 'seventy' elsif write == 8 numString = numString + 'eighty' elsif write == 9 numString = numString + 'ninety' end if left > 0 numString = numString + '-' end end write = left # で、一の桁を書き出します。 left = 0 # leftから一の桁を引きます。(0になりますね。) if write > 0 if write == 1 numString = numString + 'one' elsif write == 2 numString = numString + 'two' elsif write == 3 numString = numString + 'three' elsif write == 4 numString = numString + 'four' elsif write == 5 numString = numString + 'five' elsif write == 6 numString = numString + 'six' elsif write == 7 numString = numString + 'seven' elsif write == 8 numString = numString + 'eight' elsif write == 9 numString = numString + 'nine' end end if numString == '' # この時点で"numString"が空文字列ということは、 # もともとの数 "number" は0です。 return 'zero' end # ここまできたら、0から100までの数の英語バージョンがどこかに # しまわれています。そう、"numString"を返す必要があります。 numString end puts englishNumber( 0) puts englishNumber( 9) puts englishNumber( 10) puts englishNumber( 11) puts englishNumber( 17) puts englishNumber( 32) puts englishNumber( 88) puts englishNumber( 99) puts englishNumber(100)
zero nine ten eleven seventeen thirty-two eighty-eight ninety-nine one hundred
このプログラムに関しては、私が気に入らない点が3つほどあります。 第1に、繰り返しが多すぎです。第2に、100以上の数は扱えません。 第3に、特別扱いが沢山ありすぎます。つまりreturnが多すぎなのです。 ここで、配列を使ってもう少しきれいに仕上げてみましょう。
def englishNumber number if number < 0 # 負の数は不可です。 return '負でない数を入力してください.' end if number == 0 return 'zero' end # もう、上限として特別の場合は設けません。余分なreturnもなし。 numString = '' # これが最終的に返す文字列です。 onesPlace = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'] tensPlace = ['ten', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'] teenagers = ['eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'] # 上のほうの桁から表示させていって、"left" には # まだ残っている数を入れます。 # "write" にはこれからすぐに出力させるための # 一桁分の数を入れます。 # write と left... 良いですか? :) left = number write = left/100 # 百の桁以上の桁をwriteに入れます。 left = left - write*100 # 下2桁をleftに残します。 if write > 0 # さて、ここでちょっとしたトリックを使います。 hundreds = englishNumber write numString = numString + hundreds + ' hundred' # これは「再帰法」と呼ばれる技です。さて何をやって # いるのでしょう? 上の行ではメソッドの中で自分自身 # (訳註:つまり"englishNumber")を呼ぶように指示して # います。ただし、引数として"number"の代わりに # "write"を指定しています。 # ここで、"write"が(この時点では)、最終的に英語で # 出力すべき数の百の位以上の数であるということを # 思い出してください。 # 2行目では、"hundreds"を"numString"の後に追加して、 # さらに、' hundred'という文字列を追加しています。 # 具体的に言うと、たとえば、最初に引数を1999とする # と(つまり、"number" = 1999)、englishNumberを呼ぶ # ところでは、"write"に19が入っており、"left"には99が # 入っています。ここで最も怠惰な考えとしては、 # englishNumberがまず'nineteen'を出力して、その後に # ' hundred'を出力し、最後に残りの'ninety-nine'を # englishNumberが出力してくれれば良いわけです。 if left > 0 # この時、'two hundredfifty-one'のように # ならないためにスペースを入れます。 numString = numString + ' ' end end write = left/10 # 今度は十の桁です。 left = left - write*10 # そして十の桁をひきます。 if write > 0 if ((write == 1) and (left > 0)) # 英語では12は"twelve"であって、"tenty-two"と # 言うわけには行きません。そこで、十の桁が1の # ときは特別扱いしなければなりません。 numString = numString + teenagers[left-1] # この"-1" は、teenagers[3] が 'fourteen'で # あって、'thirteen'ではないため必要です。 # writeが1の場合、一の桁に関してはもう処理して # しまっていますので、もう出力する必要は # ありません。 left = 0 else numString = numString + tensPlace[write-1] # この"-1" も、tensPlace[3] が'forty'で'thirty'ではない # ため必要。 end if left > 0 # 'sixtyfour'とは書かずに、途中にハイフンを入れます。 numString = numString + '-' end end write = left # ここで、一の桁を書き出します。 left = 0 # この桁を引きます。(0になります。) if write > 0 numString = numString + onesPlace[write-1] # この"-1"はonesPlace[3]が'four'であって # 'three'ではないから必要。 end # 最後に"numString"を返します。 numString end puts englishNumber( 0) puts englishNumber( 9) puts englishNumber( 10) puts englishNumber( 11) puts englishNumber( 17) puts englishNumber( 32) puts englishNumber( 88) puts englishNumber( 99) puts englishNumber(100) puts englishNumber(101) puts englishNumber(234) puts englishNumber(3211) puts englishNumber(999999) puts englishNumber(1000000000000)
zero nine ten eleven seventeen thirty-two eighty-eight ninety-nine one hundred one hundred one two hundred thirty-four thirty-two hundred eleven ninety-nine hundred ninety-nine hundred ninety-nine one hundred hundred hundred hundred hundred hundred
いやー 、とても良くなりました。プログラムはかなり密度が高いので 多くのコメントを追加してあります。このプログラムは、大きな数に対しても 望み通りとはいかないまでも、とりあえず動きます。例えば最後の例の数では、 'one trillion'(訳註:1兆)と出れば理想的で、そうででなくても せめて'one million million'と出て欲しいです。(3つの例とも 間違えとはいえないのですけれど。) 実際、やろうと思えばすぐにでも 出来ますよね。
• 上のenglishNumberを拡張してみましょう。 最初にthousand(千の桁)を導入してください。上のプログラムでは 'ten hundred'となっているところを'one thousand'を返すように、 あるいは、'one hundred hundred'の代わりに'ten thousand'を 返すようにします。
• englishNumberにさらに改良を加えましょう。 milion(百万)を加えます。その結果、'one thousand thousand'の代わりに 'one million'が得られるようにします。 その後、billion(十億)とかtrillion(兆)とかを追加していってみましょう。 どこまでいけるでしょうか?
• weddingNumberはどうでしょう? このプログラムはenglishNumber とほとんど同じように動作しますが、"and" という接続詞をやたらめったら挿入します。 たとえば 'nineteen hundred and seventy and two' という具合に、結婚式の 招待状のような感じで。もう少し例を挙げたいと思いましたが、私自身詳しくありません。 結婚のコーディネータに訊いてみるのも良いでしょう。
• "99本のビールが壁に..." englishNumberと以前作ったプログラムを使って、今度は正しい 方法で この歌の詩を出力させなさい。 その後は、9999から初めてコンピュータをこらしめましょう。 (ただ、あまり大きな数字を使うと、コンピュータといえども全部出力するのにかなりの時間を 必要とします。10万本のボトルには、かなりの時間がかかります。もし、100万本のボトルなどと するとあなた自身がひどい目に会いますよ。)
おめでとう! この時点であなたは本物のプログラマと言えます! あなたは大きなプログラムを1から作成するのに必要なすべての知識を学んできました。 もし、自分のために書きたいプログラムのアイデアがあるとしたら、やってみましょう。
もちろん、すべてを1から作るというのは時間のかかる作業です。 果たして、誰かがすでに書いたプログラムをもう一度書くのに時間をかける 意味があるでしょうか? あなたの書いたプログラムがe-mailを送ったり、コンピュータ上のファイルを保存したり 読み出したりできたら良いと思いませんか? あるいは、サンプルコードがリロードするたびに実際に実行されるようなチュートリアルの Webページを作ったりはどうでしょう :)。 こういったプログラムを、より良く早く書くために、Rubyには多くの異なる オブジェクトの種類があります。