抽象クラスとモジュール

Ruby のモジュールはインスタンスが生成されないことを約束するという意味では抽象クラスです。一方、Java の抽象クラスというのは、インスタンスが生成されないことを約束する以外に、そのサブクラスに対して自身が持つ抽象メソッドの実装を強制させる役割を果たすことが多いです。

では Ruby のモジュールに、そのモジュールを include したクラスで「このメソッド実装しる」と強制させるメカニズムはあるのでしょうか、ということに興味を持ちました。

そういえば、Enumerable モジュールは Mixin 用のモジュールですが、include した側が each メソッドを持っていることを仮定したモジュールです。もし Ruby に、サブクラスあるいは Mixin したモジュールにメソッドの実行を強制できるメカニズムがあるとしたら、Enumerable では当然それを使いたくなるところです。

つまり、メソッド実装強制メカニズムがあるなら

#!/usr/local/bin/ruby

class Foo
  include Enumerable
end

Foo.new.class.each do |foo|
  foo.class.display
end

というのは(Foo に each を実装せずにモジュールを Mixin してるので) 実行時エラーではなく、コンパイルエラー(?) になるはず、ということです。

しかし、このコードは実行時エラーでした。ということで、Ruby には Java の抽象メソッドのようなメカニズムはないのだろうと思いました。

Factory Method パターン

id:hyuki さんのデザインパターン本から Factory Method パターンを移植してみます。

まずは素で移植したものです。

#!/usr/local/bin/ruby

class Factory
  def create(owner)
    p = self.create_product(owner)
    self.register_product(p)
    return p
  end
end

class IDCard
  attr_reader :owner
  def initialize(owner)
    puts "#{owner} のカードを作ります。"
    @owner = owner
  end

  def use
    puts "#{owner} のカードを使います。"
  end
end

class IDCardFactory < Factory
  attr_reader :owners
  
  def initialize
    @owners = Array.new
  end

  def create_product(owner)
    IDCard.new(owner)
  end

  def register_product(p)
    @owners.push(p.owner)
  end

  protected :create_product, :register_product
end

factory = IDCardFactory.new
products = [
  factory.create("るびお"),
  factory.create("るびこ"),
  factory.create("まっつさん")
]

products.each do |product|
  product.use;
end

factory.owners.each do |owner|
  puts owner
end

書籍では Factory クラスは抽象クラスになっていますが、ここでは普通のクラスとして実装しています。IDCardFactory はその Factory を継承します。

実行すると以下のようになります。

$ ruby factory_method.rb
るびお のカードを作ります。
るびこ のカードを作ります。
まっつさん のカードを作ります。
るびお のカードを使います。
るびこ のカードを使います。
まっつさん のカードを使います。
るびお
るびこ
まっつさん

たまたま目を通した Ruby のモジュールのマニュアルユーザーズガイド

モジュールはインスタンスを生成しない(抽象クラスであるこ とが保証される).

とあって、モジュールを抽象クラスとして利用することができることが分かりました。そこで Factory クラスをモジュールに変更しました。

#!/usr/local/bin/ruby

module Factory
  def create(owner)
    ...
  end
end

class IDCardFactory
  include Factory
  ...
end

Ruby っぽくなったし、無理矢理抽象クラスみたいなこともないのですっきりしました。抽象クラスを実現したい場合にはモジュールで、と覚えておきます。

ところで、IDCardFactory という名前が Ruby っぽくないかなと思ったので、IDCard::FactoryFactory::IDCard に変更してみたところ

factory_method.rb:33:in `initialize': wrong number of arguments (1 for 0) (ArgumentError)
        from factory_method.rb:33:in `create_product'
        from factory_method.rb:5:in `create'
        from factory_method.rb:43

とエラーになってしまいました。どうも、IDCard.new しているところで IDCard クラスではなく Factory::IDCard を対象にしてしまっているような感じです。Foo::Bar は Perl名前空間と同じようなものだと思っていましたが、ちょっと違うみたいです。スコープのメカニズムか何かが入っているのでしょうか。

ERB を使って RSS を JavaScript に変換する

ERB の使い方を覚えたので、ERB を使って RSSJavaScript に変換したいです。よくある類の処理ですが、これを ERB を使ってやってみます。

#!/usr/local/bin/ruby

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

module RSS::JavaScriptfy
  def to_html
    @erb = ERB.new(DATA.read, nil, "-")
    @erb.result(binding)
  end

  def to_js
    self.to_html.split("?n").collect { |line|
      line.gsub!(/'/, "?'")
      "document.writeln('#{line}');"
    }.join("?n")
  end
end

class RSS::RDF
  include RSS::JavaScriptfy
end

class RSS::Rss
  include RSS::JavaScriptfy
end

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

rss = RSS::Parser::parse(open(ARGV.shift).read)
puts rss.to_js

__END__
<h2><a href="<%= channel.link %>"><%= channel.title %></h2>
<ul>
<% items.each do |item| -%>
<li><a href="<%= item.link %>"><%= item.title %></a></li>
<% end -%>
</ul>

RSS::JavaScriptfy というモジュールを作って、その中に ERB で RSS オブジェクトを HTML に変換する to_html、HTML string の各行を document.write で囲む to_js メソッドを作りました。これを RSS::RDFRSS::Rss に Mixin しておき、実行側では取得した rss オブジェクトに対して rss.to_js を呼ぶという仕組みです。

$ ruby rss2js.rb http://d.hatena.ne.jp/rubyo/rss > rubyo.js

として保存し、HTML からこの rubyo.js を script タグでインクルードしたところ、

と表示されました。

ERB を使ってみる

Ruby でテンプレートエンジンを使ってみたいです。

Rails の Action View では ERB が使われるようです。まずは単体で ERB を使ってみたいと思います。ERB は Ruby 1.8 以降には標準で含まれているようなので require 'erb'するだけで使えました。

#!/usr/local/bin/ruby

require 'erb'

class Player
  def initialize(name, age)
    @name = name
    @age = age
  end

  def to_s
    @erb = ERB.new(DATA.read)
    @erb.result(binding)
  end
end

puts Player.new('rubyo', 19).to_s

__END__
name: <%= @name %>
age: <%= @age %>

<%= Time.now %>

ERB.new にテンプレートの文字列を渡します。ここでは __END__ 以降に書いた文字列を取得する DATA.read を使いました。便利です。

テンプレート上からアクセスするオブジェクトを、ERB インスタンスに渡す方法は他言語のテンプレートエンジンと比較するとちょっと特殊です。例えば Perl の HTML::Template や Template Toolkit では、テンプレートインスタンスuser => $user_object のような形で渡すことになりますが、ERB ではそれとは違い Kernel#binding を使って、ERB インスタンスをテンプレートに渡したいオブジェクトに組み込むような形で実装するようです。

なお、Kernel#binding は ri によると

Returns a +Binding+ object, describing the variable and method bindings at the point of call. This object can be used when calling +eval+ to execute the evaluated command in this environment. Also see the description of class +Binding+.

だそうです。binding を渡された ERB 側は、Binding オブジェクトを使って内部処理を進めているのでしょう。

このコードを実行すると

$ ruby erb.rb
name: rubyo
age: 19

Wed Apr 05 11:52:20 JST 2006

となりました。

Ruby の '=> '#2

まつもとさんにコメントをいただきました。びっくりしました。:foo => 'bar' と書くのはめんどくさいよ、という話をしてみたところ Ruby 1.9 では

foo: bar

と書けるそうです。なるほどー。実装されるのが楽しみです。

これを 1.8 でできるようなマジックはないのかな、と思いました。Perl には Acme::Dot など、文法そのものを変更してしまうマジカルモジュールが結構あります。まだ修行中なので、Ruby の文法そのものを変更する手段などについてはほとんど知りません。

なお、まつもとさん曰く Perl 6 では左側の Bareword を文字列として解釈しなくなるそうです。ええー。

Ruby で文字コード変換

nuna さんに教えてもらいました。1.8.2 以降では NKF#nkf を使うと良いそうです。リモートの RSS を取得して、そのタイトルを EUC-JP で出力してみます。

#!/usr/local/bin/ruby

require 'open-uri'
require 'rss/1.0'
require 'nkf'

content = Kernel.open('http://d.hatena.ne.jp/rubyo/rss')
rss = RSS::Parser::parse(content.read)

rss.items.each do |item|
  puts NKF.nkf('-m0 -e', item.title)
end

NKF.nkf('-m0 -e', item.title) のところで UTF-8 から EUC-JP に変換しています。第一引数が謎のオプションです。おそらく nkf コマンドと同一なのでしょう。こんなプログラマブルでないインタフェースでいいのでしょうか...ほかにちゃんとしたインタフェースがあるのかもしれません。

実行すると、

$ ruby euc2utf8.rb
 Ruby リファレンスマニュアル
 initialize に渡ってきたパラメタをインスタンス変数に丸投げする
 RSS を YAML に変換する#2
 Template Method パターン
 initialize に渡された値を自動的にインスタンス変数にする
 Adapter パターン
 Enumerable で拡張してみます
 Iterator パターン
 RSS を YAML に変換する
 RSS フィードを処理する
 HTTP でコンテンツを取得する
 irb で文法確認
 Ruby で YAML
 Ruby で grep
 るびおです。

と、確かに EUC-JP な端末に文字化けせずに出力させることができました。nuna さんありがとうございました。