2010年8月24日火曜日

Javascript から見る Ruby のイテレータ - Enumerable

JavaScript のクロージャ と オブジェクト指向 」のつづき

Ruby の Enumerable

Ruby ではコンテナの役割を持つクラスに Enumerable モジュールをインクルードし、その要素を順にブロックに与えるメソッド each を定義することにより、要素に対する便利なメソッドがいくつか使えるようになる。

例えば、「人」は `名前' と `年齢' を属性として持ち、 「グループ」 に所属しているとする。

# 人
class Person
  attr_reader :name, :age
  def initialize(name, age)
    @name, @age = name, age
  end
  def to_s
    @name + " " + @age.to_s
  end
end

# グループ
class Group
  include Enumerable
  def initialize
    @persons = []
  end
  def add(ps)
    @persons << ps; self
  end
  def to_s
    @persons.join(",")
  end
  def each
    @persons.each{|ps| yield ps}
  end
end

g = Group.new.
  add(Person.new("Hanako", 15)).
  add(Person.new("Jiro", 15)).
  add(Person.new("Tarou", 21))

g.each{|ps| p ps}
p g.map{|ps| ps.age * 2}                      #=> [30, 30, 42]
p g.select{|ps| ps.age < 20}                  #=> [Hanako 15, Jiro 15]
p g.inject(0){|result, ps| result + ps.age}   #=> 51
p g.inject(""){|result, ps| result + ps.name} #=> "HanakoJiroTarou"

ここで使われている yield とは、

自分で定義したブロック付きメソッドでブロックを呼び出すときに使います。 yield に渡された値はブロック記法において || の間にはさまれた変数(ブロックの引数)に代入されます。

(メソッド呼び出し - Rubyリファレンスマニュアル より)

この yield の動作はイメージしにくい。理由は、ブロックの呼び出しをするために特別な構文が用意されていることと、当の呼び出し先のブロックの名前がメソッドの定義に現れていないことにより、一体 yield がどこに何を渡しているのかわかりずらいため。

 

Javascript で enumerable 関数を定義

ところで、前回は 「JavaScript のクロージャ と オブジェクト指向」 について見た。今回はこの延長線上で、上記 Ruby の Enumerable のような動作をする関数を定義してみる。

まずは、「人」 と 「グループ」 オブジェクトを生成する関数を定義。はじめは 「グループ」 に 「人」 を追加する関数を追加。

/**
 * 人
 */
var person = function(spec){
    var that = {};
    
    var toString = function(){
        return spec.name + " " + spec.age.toString();
    };
    that.toString = toString;
    
    var getName = function(){
        return spec.name;
    };
    that.getName = getName;
    
    var getAge = function(){
        return spec.age;
    };
    that.getAge = getAge;
    
    return that;
};

/**
 * グループ
 */
var group = function(spec){
    var that = {};
    var persons = [];
    
    var add = function(ps){
        persons[persons.length] = ps;
        return that;
    };
    that.add = add;
    
    var toString = function(){
        return persons.join(",");
    };
    that.toString = toString;
    
    return that;
};

var g = group({}).add(person({
    name: "Hanako",
    age: 15
})).add(person({
    name: "Jiro",
    age: 15
})).add(person({
    name: "Tarou",
    age: 21
}));

console.log(g.toString());   // Hanako 15,Jiro 15,Tarou 21

 

group 関数が返すオブジェクトに enumerable 関数を定義

次に Ruby でコンテナクラスに要素を順にブロックへと渡すメソッド each を定義したのと同様に、 「グループ」 オブジェクトに関数 each を定義してみる。

group 関数内…

    var each = function(f){
        var i;
        for (i = 0; i < persons.length; i++) {
            f(persons[i]);
        }
    };
    that.each = each;

Ruby での定義は以下の通りだった。

  def each
    @persons.each{|ps| yield ps}
  end

違いは Javascript では関数 f が明示されており、この f が Ruby のブロックに相当。 yield が関数 f の呼び出しに当たる。このように比較すると、Ruby の yield の意図が汲み取りやすく、呼び出しの関係もイメージしやすい。

先ほど定義した変数 g で each 関数を使ってみる。

g.each(function(ps){
    console.log(ps.toString());
});

結果は、

Hanako 15
Jiro 15
Tarou 21

 

map, filter, fold の定義

上記 each 関数を使い、map, filter, fold を goup 関数内に定義する。

    var map = function(f){
        var result = [];
        each(function(ps){
            result[result.length] = f(ps);
        });
        return result;
    };
    that.map = map;
    
    var filter = function(p){
        var result = [];
        each(function(ps){
            if (p(ps)) {
                result[result.length] = ps
            }
        });
        return result;
    };
    that.filter = filter;
    
    var fold = function(z, f){
        var acc = z;
        each(function(ps){
            acc = f(acc, ps);
        });
        return acc;
    };
    that.fold = fold;

これを使うと、

var g2 = g.map(function(ps){
    return ps.getAge() * 2;
});
console.log(g2.toString());          // 30,30,42

var g3 = g.filter(function(ps){
    return ps.getAge() < 20;
});
console.log(g3.toString());          // Hanako 15,Jiro 15

var g4 = g.fold(0, function(acc, ps){
    return acc + ps.getAge();
});
console.log(g4.toString());          // 51

var g4 = g.fold("", function(acc, ps){
    return acc + ps.getName();
});
console.log(g4.toString());          // HanakoJiroTarou

ここまでのコード

 

enumerable 関数に map, filter, fold を移す

map, filter, fold は group 関数に特有のものではない。Ruby のモジュールのように enumerable 関数を新たに作りその中 に 3 つの関数を移す。これにより、他のコンテナオブジェクトを生成する関数で共通に利用できるようになる。

以下の enumerable 関数の引数 that は、3 つの関数を追加するオブジェクトを表わす。

/**
* Enumerable - コンテナが要素に対して行う処理。
* コンテナで each メソッドが定義されていることを要求する。
*/
var enumerable = function(that){
    var map = function(f){
        var result = [];
        that.each(function(e){
            result[result.length] = f(e);
        });
        return result;
    };
    that.map = map;
    
    var filter = function(p){
        var result = [];
        that.each(function(e){
            if (p(e)) {
                result[result.length] = e
            }
        });
        return result;
    };
    that.filter = filter;
    
    var fold = function(z, f){
        var acc = z;
        that.each(function(e){
            acc = f(acc, e);
        });
        return acc;
    };
    that.fold = fold;
    
    return that;
};

group 関数内では、最終的に返すオブジェクト that を enumerable 関数に渡し、that のプロパティに map, filter, fold メソッドを追加してもらうように変更する。 Ruby でモジュールをインクルードすることに相当。

var group = function(spec){
    var that = {};
    that = enumerable(that);

ここまでのコード。

 

関連記事