initialize に渡ってきたパラメタをインスタンス変数に丸投げする

の続きです。ささださんに教えてもらいました。有名な奥の手、だそうです。

#!/usr/local/bin/ruby

class Player
  attr :name, :age
  define_method(:initialize) { |@name, @age| }
end

player = Player.new('Rubyo', 19)
player.name.display

たしかにこれで "Rubyo" と表示されました。define_method は Kernel モジュールに実装されているプライベートメソッドでしょうか。Kenerl.define_method としたところ

class.rb:5: private method `define_method' called for Kernel:Module (NoMethodError)

と出ました。ri define_method では

Defines an instance method in the receiver. The _method_ parameter can be a +Proc+ or +Method+ object. If a block is specified, it is used as the method body. This block is evaluated using +instance_eval+, a point that is tricky to demonstrate because +define_method+ is private. (This is why we resort to the +send+ hack in this example.)

とありました。"ブロックが指定されるとそれはメソッド本体として扱われる。このブロックは instance_eval によって eval される" のあたりがこのトリックの種明かしのようです。

ささださんによると 2.0 では使えなくなるかも、とのことです。ささださんありがとうございました。

RSS を YAML に変換する#2

の続きです。nuna さんがコメントで色々教えてくれました。

  • バージョンが異なるフィードを、バージョンを意識せずに parse するには require を複数書くとよい (http://www.cozmixng.org/~rwiki/?cmd=view;name=RSS+Parser%3A%3ATutorial.ja にマニュアルあり。)
  • YAML にするときにオブジェクトをそのまま渡すと、YAML で復元した側がそのオブジェクトのクラスを必要とするようになってしまう。これを回避するには RSS オブジェクトを Hash にすると良い。
  • Hash にするには RSS::RDF に to_hash などを追加定義するとよい

とのことです。そこで、昨日のコードを書き替えてみました。

  • require で rss/1.0 に加えて rss/2.0 も追加したので、RSS 2.0 を parse できるようになりました。
  • to_hash メソッドの追加は RSS::Hashable というモジュールを使って Mixin でやってみました。RSS::RDF (1.0 用のクラス) と RSS::Rss に全く同じメソッドを追加する必要があったので。
#!/usr/local/bin/ruby

require 'open-uri'
require 'rss/1.0'
require 'rss/2.0'
require 'yaml'

module RSS::Hashable
  def to_hash
    {
      :channel => {
        :title       => self.channel.title,
        :link        => self.channel.link,
        :description => self.channel.description
      },
      :items => self.items.map do |item|
        {
          :title       => item.title,
          :link        => item.link,
          :description => item.description
        }
      end
    }
  end
end

class RSS::RDF
  include RSS::Hashable
end

class RSS::Rss
  include RSS::Hashable
end

class RSS2YAML
  def initialize(url)
    @url = url
  end

  def to_hash
    rss = self.parse_rss(@url)
    rss.to_hash
  end

  def to_yaml
    self.to_hash.to_yaml
  end

  def get_remote(url)
    begin
      return Kernel.open(@url).read
    rescue OpenURI::HTTPError
      return nil
    end
  end

  def parse_rss(url)
    content = self.get_remote(url)
    if content
      begin 
        return RSS::Parser::parse(content)
      rescue RSS::InvalidRSSError
        return nil
      end
    end
  end

  public :to_yaml
  protected :get_remote, :parse_rss
end

if ARGV.size != 1
  puts "Usage: ruby rss2yaml.rb <url>"
  exit
end

puts RSS2YAML.new(ARGV.shift).to_yaml

これで出力の yamlruby に依存しなくなりました。nuna さんありがとうございました。

RSS::Hashable の中身がちょっと Perl チックな書き方なのですが、こういうものでしょうか。Ruby は既存のクラスにメソッドを追加したりモジュールを Mixin したりするのが楽でいいですね。

ところで「たのしい Ruby」には普通は"モジュールの中では self は使いません" という記述があるのですが、Mixin 用モジュールでは使わないと書けないようなきもしますがどうなんでしょう。

Ruby の '=> '

ハッシュのキーと値のペアを記述するときなどに

:key => 'value'

とします。このとき左側はほとんどの場合シンボルのような気がします。なので、'=>' が与えられた場合左側が Bareword だった場合は自動的にシンボルとして扱ってくれてもいいのになあ、と思うことがよくあります。

ちなみに Perl では '=>' の左側が Bareword の時は、自動的に文字列として解釈されます。

...あ、でも Ruby ではハッシュのキーにオブジェクトが使えるらしいから、そういうわけでもないのかもしれません。まだ修行が足りません。

O/R マッパ(Active Record) を使ってみる

Ruby で O/R マッパを使って RDBMS を操作したいです。

Ruby on Rails に付属してくる O/R マッパの Active Record を使ってみます。Rails は元々インストールしていたので、そこから Active Record 単体で使ってみます。

mysql に、

+-------------+------------------+------+-----+---------+----------------+
| Field       | Type             | Null | Key | Default | Extra          |
+-------------+------------------+------+-----+---------+----------------+
| id          | int(10) unsigned |      | PRI | NULL    | auto_increment |
| url         | varchar(255)     | YES  |     | NULL    |                |
| title       | varchar(255)     | YES  |     | NULL    |                |
| description | text             | YES  |     | NULL    |                |
+-------------+------------------+------+-----+---------+----------------+

というテーブルがあります。このテーブルは Active Record を使うつもりで設計したものなので、auto_increment な id カラムを持ち、テーブル名は "items" と複数形にしてあります。

#!/usr/local/bin/ruby

require 'rubygems'
require 'active_record'

class Item < ActiveRecord::Base
end

ActiveRecord::Base.establish_connection(
  :adapter  => 'mysql',
  :host     => 'localhost',
  :username => 'rubyo',
  :password => '',
  :database => 'bookmark'
)

とりあえずこれで、Active Record で bookmark データベースに接続して item テーブルを操作することができます。最初 require 'active_record' とだけ書いていて、ライブラリが見つからなくて少しはまりました。rubygem を使ってインストールしたライブラリを使うには require 'rubygems' も併記しておかないといけないようです。

手始めに、

#!/usr/local/bin/ruby

require 'rubygems'
require 'active_record'

class Item < ActiveRecord::Base
end

ActiveRecord::Base.establish_connection(
  :adapter  => 'mysql',
  :host     => 'localhost',
  :username => 'rubyo',
  :password => '',
  :database => 'bookmark'
)

Item.create(
  :url         => 'http://d.hatena.ne.jp/rubyo/',
  :title       => 'るびおの日記',
  :description => 'るびおのRuby勉強日記'
);

Item.create(
  :url         => 'http://d.hatena.ne.jp/rubyco/',
  :title       => 'るびこの日記',
  :description => 'るびこのRuby勉強日記'
);

Item.create(
  :url         => 'http://b.hatena.ne.jp/rubyo/',
  :title       => 'るびおのブックマーク',
  :description => 'るびおのRuby関連ブックマーク'
);

として items に三つのレコードを追加しました。

次に items テーブルに検索系のクエリを投げてみました。

Item.find(:all, :limit => 2, :order => "id desc").each do |item|
  puts item.url
end

Item.find(:all, :conditions => "url like '%ruby%'").each do |item|
  puts item.url
end

puts Item.count
puts Item.count(['title = ?', 'るびおの日記'])

Item.find_all_by_title('るびおの日記').each do |item|
  puts item.url
end

この結果は、

http://b.hatena.ne.jp/rubyo/
http://d.hatena.ne.jp/rubyco/
http://d.hatena.ne.jp/rubyo/
http://d.hatena.ne.jp/rubyco/
http://b.hatena.ne.jp/rubyo/
3
1
http://d.hatena.ne.jp/rubyo/

となりました。Active Record はとても直感的で分かりやすいと思いました。Active Record について詳しくは Rails 本の P.207 から解説がありました。しっかり読んで理解したいと思います。

RailsによるアジャイルWebアプリケーション開発

RailsによるアジャイルWebアプリケーション開発

Enumerable で拡張してみます

ruby には Enumerable というモジュールがあり、集合を扱うクラス(コレクション)にこのモジュールを mix-in すると、Enumerable が備える各種メソッドが使えるようになるそうです。例えば

  • collect
  • find_all
  • map
  • grep

など各種メソッドが使えます。そこで、先の Iterator のコードの BookShelf を Enumerable を使って拡張してみたいと思います。

Enumerable を mix-in するにはウサギ本によるとコレクションで each メソッドを定義すれば良いようです。(Enumerable#max, #min, #sort を使うには <=> 演算子も定義する必要があります。) 先の BookShelf はすでに each メソッドを持っていますので、

class BookShelf
  include Enumerable
  def initialize
    @books = Array.new
  end
  
  def append_book(book)
    @books.push(book)
  end

  def each
    @books.each do |book|
      yield(book)
    end
  end
end

include Enumerable を追加するだけで良かったです。これで Enumerable のメソッドが使えるようになりましたので、

(shelf.find_all do |book| book.name =~ /Daddy/i; end).each do |book|
  puts book.name
end

こんなコードを実行することができました。

$ ruby iterator.rb
Daddy-Long-Legs

プログラミングRuby―達人プログラマーガイド

プログラミングRuby―達人プログラマーガイド

Iterator パターン

id:hyuki さんのデザインパターン本の Iterator パターンを Ruby に移植してみます。

#!/usr/local/bin/ruby

class Iterator
  def has_next
  end
  def next
  end
end

class Aggregate
  def iterator
  end
end

class Book
  attr_reader :name
  def initialize(name)
    @name = name
  end
end

class BookShelf < Aggregate
  def initialize
    @books = Array.new
    @last = 0
  end
  
  def [](index)
    @books[index]
  end

  def append_book(book)
    @books[@last] = book
    @last += 1
  end

  def length
    @last
  end

  def iterator
    BookShelfIterator.new(self)
  end
end

class BookShelfIterator < Iterator
  def initialize(bookshelf)
    @bookshelf = bookshelf
    @index = 0
  end

  def has_next
    if (@index < @bookshelf.length)
      return true
    else
      return false
    end
  end

  def next
    book = @bookshelf[@index]
    @index += 1
    return book
  end
end

shelf = BookShelf.new
shelf.append_book(Book.new("Around the World in 80 Days"))
shelf.append_book(Book.new("Bible"))
shelf.append_book(Book.new("Cinderella"))
shelf.append_book(Book.new("Daddy-Long-Legs"))

it = shelf.iterator
while (it.has_next) 
  book = it.next
  puts book.name
end

これを実行すると

$ ruby iterator.rb
Around the World in 80 Days
Bible
Cinderella
Daddy-Long-Legs

となります。

Java のコードをほぼそのままで getBookAt(int index)[](index) にしてるところが違うぐらいのコードです。でもこれですと、

  • Iterator クラス、Aggregate クラスはコードを実行する上では全く意味を為していない
    • Java の場合は interface として書けるので意味がある
  • while 文でイテレータを回すあたりが ruby らしくない

という感じがします。そこで http://www.freeml.com/message/patterns/997 で高橋さんが書いたコードのように each を使うようにしてみます。

#!/usr/local/bin/ruby

class Book
  attr_reader :name
  def initialize(name)
    @name = name
  end
end

class BookShelf
  def initialize
    @books = Array.new
  end
  
  def append_book(book)
    @books.push(book)
  end

  def each
    @books.each do |book|
      yield(book)
    end
  end
end

shelf = BookShelf.new
shelf.append_book(Book.new("Around the World in 80 Days"))
shelf.append_book(Book.new("Bible"))
shelf.append_book(Book.new("Cinderella"))
shelf.append_book(Book.new("Daddy-Long-Legs"))

shelf.each do |book|
  puts book.name
end

shelf.each do |book| ... end で回せるようになって ruby らしいコードになりました。BookShelfIterator として機能していた箇所は yield を使った each の中に内包されたので、クラスは二つだけになりました。

高橋さんのコードの最終形にもあるとおり、この場合の BookShelf は Array を継承するだけでも良かったりします。ただし、その場合 BookShelf がデータ構造を抽象化していることにはならないのかもしれません。

yield がすぐに頭に出てくるようになるにはもっと修行が必要そうです。

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門