原始的Ruby入門2回目Fizzbuzzの回

一昨日のつづきね。

こないだのHello Worldが、プログラミング言語を学ぶための儀式であるならば、最近のプログラミング言語の学習でこれでも書いとけ的な問題としてFizzBuzz問題というのがある。

1から100までの数字で、3の倍数のときはアホになるFizzと、5の倍数のときはBuzzと、15の倍数はFizzBuzzと、その他の数字は数字をそのまま表示せよという問題。

まず、RubyというのはCの皮をかぶったLispなので、Lisp脳らしく、1から100までの配列を作る。Rubyでは

[1,2,3,....,99,100]

のように、配列のオブジェクトを生成する簡単な記法が存在するのだが、いちいち100まで書くのは面倒なので、Rangeオブジェクトというのを使い、そこから配列クラスを生成する。

Rangeオブジェクトというのは、指定した範囲を表すオブジェクトである。Rangeオブジェクトも配列と同じで、

(1..100)

のように簡単に生成できる。そのオブジェクトがもつto_aメソッドを使うと、その範囲を要素ともつ配列が作られる。こんなかんじ

VALUE ary = rb_funcall(rb_eval_string("(1..100)"),rb_intern("to_a"),0);

rb_eval_string()というのは、おまじないではなく、錬金術である。あやしさと等価交換で便利さを得ることができる。

この配列のそれぞれの要素に対し、3,5,15の倍数かどうかを判断して、指定された文字を表示すればいい。

Javaなどでは、配列の要素をすべて回すには、for文等で配列の長さをみながらまわしていくが、Rubyでは、イテレーターという便利な物がある。配列の持つイテレーターとして、代表的なのがeachメソッドである。eachメソッドは、配列の要素すべてに対し、ブロックと呼ばれる物に要素をわたして、実行していく。ブロックというのは、簡単に言うと、一時的な関数と思ってくれていい。

というわけで、戦略としてブロック内で、表示する文字を判別し表示するという戦略をとる。

では、今回使用するブロックを見てみよう。

VALUE
fizzbuzz( VALUE elem, VALUE nil)
{
    int mod  = rb_intern("%");
    int eql  = rb_intern("==");
    int puts =  rb_intern("puts");
    int kuku =  rb_intern("<<");
    int length =  rb_intern("length");
    int to_s =  rb_intern("to_s");
    VALUE str_buff = rb_str_new2("");

    if (rb_funcall(rb_funcall(elem,mod,1,INT2NUM(3)),eql,1,INT2NUM(0))) 
        rb_funcall(str_buff,kuku,1,rb_str_new2("Fizz"));

    if (rb_funcall(rb_funcall(elem,mod,1,INT2NUM(5)),eql,1,INT2NUM(0))) 
        rb_funcall(str_buff,kuku,1,rb_str_new2("Buzz"));

    if (rb_funcall(rb_funcall(elem,length,0),eql,1,INT2NUM(0))) 
        rb_funcall(str_buff,kuku,1,rb_funcall(elem,to_s,0));
    
    return rb_funcall($kernel,puts,1,str_buff);
}

ブロックは、オブジェクトをもらって、オブジェクトを返す関数のようなものである。
まずは、復習。rb_funcallというのは、第一引数のオブジェクトに対し、第二引数のメッセージをよびだせやという命令である。Rubyでは、rb_funcallでメソッド呼び出しを行うと、オブジェクトが返ってくる。しかも、大抵これが返ってくるとうれしいなというオブジェクトが返ってくる。わざわざ、流れるようなインターフェースとかを意識しなくてもストレス無く書ける。
最後のreturnの部分で出てくる$kernelというのは、グローバル変数である。Rubyでは、グローバル変数は、$からはじまることになる。今回は、$kernelというグローバル変数にObjectクラスのインスタンスを生成している(別の箇所だけど)。
処理の流れを説明すると、空の文字列を用意し、渡された要素(elem)が3の倍数ならくくメソッド(<<)を使い、文字列に"Fizz"を追加。次に5の倍数なら"Buzz"を追加。最後に文字列の長さが0なら、要素を文字列になおした(to_sメソッドを使う)ものを文字列に追加。

さて、いよいよeachに今作ったブロック(fizzbuzz)をわたして呼び出してみよう。

rb_iterate(rb_each,ary,fizzbuzz,Qnil);

メソッド呼び出しの時は、オブジェクト、メソッドの順であったが、イテレーターメソッド呼び出しの時は、イテレーターメソッド、オブジェクト、ブロックの順になるので注意。オブジェクトに対し、イテレーターメソッドを呼び出し、イテレーターメソッド内の特定のタイミング(今は全然わからなくていい)で、要素を引数にしてブロックが実行されることになる。(実は、第四引数もわたされるんだが、今は考えなくていい)。

では、今回のプログラム全体。今回もOMAZINAIいっぱい。

#include "ruby.h"

VALUE $kernel ;

VALUE
fizzbuzz( VALUE elem, VALUE nil)
{
    int mod  = rb_intern("%");
    int eql  = rb_intern("==");
    int puts =  rb_intern("puts");
    int kuku =  rb_intern("<<");
    int length =  rb_intern("length");
    int to_s =  rb_intern("to_s");
    VALUE str_buff = rb_str_new2("");

    if (rb_funcall(rb_funcall(elem,mod,1,INT2NUM(3)),eql,1,INT2NUM(0))) 
        rb_funcall(str_buff,kuku,1,rb_str_new2("Fizz"));

    if (rb_funcall(rb_funcall(elem,mod,1,INT2NUM(5)),eql,1,INT2NUM(0))) 
        rb_funcall(str_buff,kuku,1,rb_str_new2("Buzz"));

    if (rb_funcall(rb_funcall(elem,length,0),eql,1,INT2NUM(0))) 
        rb_funcall(str_buff,kuku,1,rb_funcall(elem,to_s,0));
    
    return rb_funcall($kernel,puts,1,str_buff);
}

int main()
{
    VALUE ary;

    ruby_init();
    ruby_init_loadpath();
    ruby_script("hello_ruby");
    $kernel = rb_class_new_instance(0,0,rb_cObject);
    ary = rb_funcall(rb_eval_string("(1..100)"),rb_intern("to_a"),0);
    rb_iterate(rb_each,ary,fizzbuzz,Qnil);

    ruby_finalize();
    return 0;
}

では、実行。

$ gcc -o fizzbuzz -I./ruby-1.8.6-p114/ -g -L./ruby-1.8.6-p114/ -lruby -ldl fizzbuz.c && ./fizzbuzz
(eval): [BUG] Bus Error
ruby 1.8.6 (2007-09-24) [universal-darwin9.0]

Abort trap

おっと。Rubyがどうやら落ちたようだ。プログラムにはバグがつきものなので、気は落とさずにデバッグしていこう。

Rubyには、ちゃんとデバッガーがついているので安心だ。シェルで次のように入力だ。

$ gdb ./fizzbuzz

そしたら、とりあえず以下のようにプログラムを実行してみよう。

(gdb) run
Starting program: /Users/taka/prog/c/hello_ruby/fizzbuzz 
Reading symbols for shared libraries +++. done

Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_PROTECTION_FAILURE at address: 0x00000048
0x000d0c98 in rb_iterate ()

どうやら、イテレーターを実行している延長で落ちているっぽい。こんなときは、大抵ブロック内の実装が悪いので、ブロックにブレークポイントをしていしてもう一度実行だ。

(gdb) b fizzbuzz
Breakpoint 1 at 0x1cd1: file fizzbuz.c, line 8.
(gdb) run
Starting program: /Users/taka/prog/c/hello_ruby/fizzbuzz 
Breakpoint 1, fizzbuzz (elem=3, nil=4) at fizzbuz.c:8
8	    int mod  = rb_intern("%");
(gdb) n
9	    int eql  = rb_intern("==");
(gdb) n
10	    int puts =  rb_intern("puts");
(gdb) n
11	    int kuku =  rb_intern("<<");
(gdb) n
12	    int length =  rb_intern("length");
(gdb) n
13	    int to_s =  rb_intern("to_s");
(gdb) n
14	    VALUE str_buff = rb_str_new2("");
(gdb) n
16	    if (rb_funcall(rb_funcall(elem,mod,1,INT2NUM(3)),eql,1,INT2NUM(0))) 
(gdb) n
19	    if (rb_funcall(rb_funcall(elem,mod,1,INT2NUM(5)),eql,1,INT2NUM(0))) 
(gdb) n
22	    if (rb_funcall(rb_funcall(elem,length,0),eql,1,INT2NUM(0))) 
(gdb) n
0x000de055 in rb_thread_trap_eval ()
(gdb) 

どうやら、文字列の長さを比較しようとしたところが悪いらしい。うーん。

おっと。文字列のlengthメソッドを呼び出そうとしているのに、配列の要素のオブジェクトに対してlengthメソッドを呼び出しているじゃないか。というわけで、こんな感じで修正。

: diff -bup  bug.fizzbuz.c fizzbuz.c
--- bug.fizzbuz.c	2008-04-18 00:25:57.000000000 +0900
+++ fizzbuz.c	2008-04-18 00:31:20.000000000 +0900
@@ -19,7 +19,7 @@ fizzbuzz( VALUE elem, VALUE nil)
     if (rb_funcall(rb_funcall(elem,mod,1,INT2NUM(5)),eql,1,INT2NUM(0))) 
         rb_funcall(str_buff,kuku,1,rb_str_new2("Buzz"));
 
-    if (rb_funcall(rb_funcall(elem,length,0),eql,1,INT2NUM(0))) 
+    if (rb_funcall(rb_funcall(str_buff,length,0),eql,1,INT2NUM(0))) 
         rb_funcall(str_buff,kuku,1,rb_funcall(elem,to_s,0));
     
     return rb_funcall($kernel,puts,1,str_buff);

よし、実行してみよう。長いので改行は、コンマに置き換えてのせるね。

$ gcc -o fizzbuzz -I./ruby-1.8.6-p114/ -g -L./ruby-1.8.6-p114/ -lruby -ldl fizzbuz.c && ./fizzbuzz
1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz,16,...,97,98,Fizz,Buzz

今回は長くなってしまったけど、だいじょうだろうか。
イテレーターやブロックというのは、Rubyではよく使うし、便利なものなので、よく復習しておこう。

終わりに:どう考えても出オチのネタを引っ張るのはしんどすぎるだろJK。今回の収穫としては、自分がよくわかってなかったイテレーターの呼び出しが、簡単にとはいえ理解できたことだろう。