使いやすいRubyのオブジェクト
Eric Hodel
Ruby Quickrefを補足するためオブジェクト同士を円滑に作用させる様々な方法についての記事です。ほとんどの例でRubyGemsのコードを使用しました。そのため、いくつかの例では次のリリースの
RubyGemsまで動作しない場合があります。
Enumerable
Enuerableモジュールは#eachメソッドと#mapや#each_with_indexといったよく知られたメソッドで構成されています。このモジュールをmix-inしたオブジェクトで#<=>メソッドが実装されていれば、#sort、#min、#maxなどの便利なメソッドが利用できます。
Gem::SourceInexでは、#eachメソッド経由で公開されているHashオブジェクトを内部的に次のように持っています:
class Gem::SourceIndex
include Enumerable
# ...
def each(&block)
@gems.each(&block)
end
end
これは次のような場合に便利です:
dep = Gem::Dependency.new ARGV.shift, Gem::Requirement.default
found = Gem.source_index.any? do |name, spec|
dep =~ spec
end
puts "found gem for #{dep.name}!" if found
Comparable
Comparableモジュールは#<=>メソッドを元に構成されており、種々の比較メソッドを提供します。
Gem::Specificationオブジェクトは、名前、バージョン、そして、プラットフォーム順でソートされます:
class Gem::Specification
include Comparable
def <=>(other)
my_platform = Gem::Platform::RUBY == @platform ? -1 : 1
other_platform = Gem::Platform::RUBY == other.platform ? -1 : 1
[@name, @version, platform] <=>
[other.name, other.version, other.platform]
end
end
このメソッドは、「gem list」コマンドのように画面上に表示する際や内部的にgemをインストールする際にオブジェクトをソートするためにRubyGemsで使用されています。
上記のコードにおいて繰り返しを避ける方法や他のソートアルゴリズムを使っての速度を向上する方法については、他の記事の#sort_by and #sort_objを読んで下さい。
#to_sと#inspectメソッド
#to_sや#inspectメソッドを上書きすることで誰かがあなたのオブジェクトを参照したい場合にスクリーン一杯に不要な情報を出力しないようにすることができます。 ある意味、出力される情報の量を制限することでデバックの助けとなります。
Gem::Specificationの#to_sメソッドはたった2つの重要な属性のみを出力します:
class Gem::Specification
def to_s
"#<gem::specification name=#{@name} version=#{@version}>"
end
end
Gem::Platformの#to_sメソッドは次のように読みやすい文字列を返します:
class Gem::Platform
def to_a
[@cpu, @os, @version]
end
def to_s
to_a.compact.join '-'
end
end
Gem::Versionの#inspectメソッドは、内部のインスタンス変数をすべて無視して、@versionのみを出力します:
class Gem::Version
def inspect # :nodoc:
"#<#{self.class} #{@version.inspect}>"
end
end
case文での比較の#===と正規表現比較の#=~メソッド
#===メソッドは一般的によくオーバーライドされますが、#=~メソッドを実装することは、可読性を向上するのに有益になる場合があります。
Gem::Platformでは#===メソッドを次のようにオーバーライドします:
class Gem::Platform
def ===(other)
return nil unless Gem::Platform === other
# cpu
(@cpu == 'universal' or other.cpu == 'universal' or @cpu == other.cpu) and
# os
@os == other.os and
# version
(@version.nil? or other.version.nil? or @version == other.version)
end
end
つまり、もし、2つのGems::Platformオブジェクトが、同じCPU(アーキテクチャー)、もしくは、どちらかがユニバーサルであり、バージョン情報がある場合にお互いが同じバージョンであれば、それらのオブジェクトは同じであることになります。
次のようにgemパッケージをグループ化することができます:
platform_count = Hash.new 0
Gem.source_index.each do |name, spec|
case spec.platform
when Gem::Platform.new('linux') then
platform_count['linux'] += 1
# ...
else
platform_count['other'] += 1
end
end
p platform_countGem::Dependencyは#=~メソッドをオーバーライドします。次のように右辺をGem::Dependencyに変換するので少し奇異に見えます:
class Gem::Dependency
def =~(other)
other = case other
when self.class then
other
else
return false unless other.respond_to? :name and
other.respond_to? :version
Gem::Dependency.new other.name, other.version
end
pattern = @name
pattern = /\A#{Regexp.escape @name}\Z/ unless Regexp === pattern
return false unless pattern =~ other.name
reqs = other.version_requirements.requirements
return false unless reqs.length == 1
return false unless reqs.first.first == '='
version = reqs.first.last
version_requirements.satisfied_by? version
end
endこのメソッドは次のようにフィルタとしても使うことができます:
dep = Gem::Dependency.new(/ruby/, Gem::Requirement.default)
ruby_named = Gem.source_index.select do |name, spec|
dep =~ spec
end
p ruby_named.map { |name, spec| name }
Hashオブジェクトのキーとして
Rubyは2つの異なるオブジェクトが同じハッシュのキーであるかを判定するために#hashと#eql?メソッドを使用します。
次のようにGem::Versionでは、内部のバージョンを表す文字列を元にハッシュのキーとして利用できるようになっています:
class Gem::Version
def hash
@version.hash
end
def eql?(other)
self.class === other and @version == other.version
end
end
Gem::Versionでは、内部のバージョンを表す文字列は「1.3」や「1.3.0」のようになっています。この実装において、2つのバージョン文字列は異なるハッシュのキーとして扱われます。2つのハッシュのキーが同じであることを判定するために#==メソッドの代わりに#eql?メソッドを使用することは重要な特徴です。それは(とても便利だと思いませんが)興味深い挙動を表すためです。Gem::Versionでは、「1.3」というバージョンは「1.3.0」というバージョンと同じですが、ハッシュ内では異なるスロットに格納されています。
#intialize_copyメソッド
#initialize_copyメソッドは、#dupや#cloneメソッドがインスタンス変数以外のオブジェクト固有の状態をコピーする際に呼び出されます。複製元のオブジェクトは新しいインスタンスへ渡されます。#initialize_copyメソッドは、次のようにオブジェクト毎に持っているキャッシュを空にするために利用されることもあります:
def initialize_copy(other) @cache = [] end
#exceptionメソッド
#exceptionメソッドは、#raiseメソッドへ渡されるオブジェクトをExceptionオブジェクトへときゃすとするために呼び出されます。このメソッドはExceptionクラスのサブクラスを返す必要があります。オブジェクトをExceptionオブジェクトに変換するためにこのメソッドを使うことができ、次のようにこのメソッドに全ての例外発生コードを集約することができます:
class Result
class Error < RuntimeError; end
def initialize(json)
@result = JSON.parse json
end
def exception(message = nil)
Error.new "#{message} (#{@result['error']})"
end
def [](key)
@result[key]
end
end
r = Result.new open('http://example.com/api/blah').read
raise r if r['error']
Marshalクラス
Rubyはほとんどのオブジェクトを自動的に直列(Marshal)化します。しかしながら、再構築することができるキャッシュデータや保存しているデータのサイズを縮めるためにカスタムの形式が必要な場合があります。そうするためには2つの方法があります。古い方法としては、#_dumpと::_loadメソッドを使用する方法で、新しい方法は、優先度が高い#marshal_dumpと#marshal_loadメソッドを使用する方法です。新しい方法へ移行したい場合においても、古い方法で直列化されたオブジェクトを戻すために::_loadメソッドをそのままにしておくこともできます。
古い方法の#_dumpメソッドを使うと直列化されたオブジェクトが(通常はMarshal化された)文字列形式が返ります。そして、古い方法の::_loadメソッドを使って、その文字列形式を戻します。::_loadはクラスメソッドであるため、オブジェクトを作成する必要があり、あるインスタンスでは重要である場合があることに留意して下さい。
新しい方法の#marshal_dumpメソッドを使うとObjectのインスタンスが返ります。そして、#marshal_loadメソッドはそのインスタンスを受け取ります。そのインスタンスはすでに生成されていますが、#initializeメソッドは呼び出されません。新しい方法では、既存のシンボルやオブジェクトの参照テーブルを利用するため、より小さいサイズの直列化されたデータをもたらします。
Gem::Specificationでは、#_dump/::loadを使用しており、下位および上位互換性であるように設計されているためにかなり複雑です。若干、簡略化した#_dump/::loadメソッドの実装を次に示します:
class Gem::Specification
CURRENT_SPECIFICATION_VERSION = 2
# number of fields per version
MARSHAL_FIELDS = { -1 => 16, 1 => 16, 2 => 16 }
def self._load(str)
array = Marshal.load str
spec = Gem::Specification.new
spec.instance_variable_set :@specification_version, array[1]
# validate object
current_version = CURRENT_SPECIFICATION_VERSION
field_count = if spec.specification_version > current_version then
spec.instance_variable_set :@specification_version,
current_version
MARSHAL_FIELDS[current_version]
else
MARSHAL_FIELDS[spec.specification_version]
end
if array.size < field_count then
raise TypeError, "invalid Gem::Specification format #{array.inspect}"
end
# restore object
spec.instance_variable_set :@rubygems_version, array[0]
# ...
spec.instance_variable_set :@platform, array[16].to_s
spec.instance_variable_set :@loaded, false
spec
end
def _dump(limit)
Marshal.dump [
@rubygems_version,
@specification_version,
# ...
@new_platform,
]
end
end
Gem::Versionは、#marshal_dump/#marshal_loadを実装しており、内部のインスタンス変数は無視して、@version変数のみを直列化します。
class Gem::Version
def marshal_dump
[@version]
end
def marshal_load(array)
self.version = array[0]
end
end
PPによる出力整形化
ちょっとした工夫で、PPを使って簡単にオブジェクトを読みやすい出力を生成することができます。その出力をコピーして、スクリプト内にペーストすることも可能です。まず、自作のオブジェクトに#pretty_printメソッドを実装して、そこで、PrittyPrint#group、PrettyPrint#text、PrettyPrint#breakable、そして、PP#ppメソッドを使用します。riを使って、これらのメソッドの文書を参照することができます。Gem::DependencyとGem::Requirementの#pretty_printメソッドを次に示します:
class Gem::Dependency
def pretty_print(q)
q.group 1, 'Gem::Dependency.new(', ')' do
q.pp @name
q.text ','
q.breakable
q.pp @version_requirements
q.text ','
q.breakable
q.pp @type
end
end
end
class Gem::Requirement
def pretty_print(q)
q.group 1, 'Gem::Requirement.new(', ')' do
q.pp as_list
end
end
def as_list
normalize
@requirements.map do |op, version| "#{op} #{version}" end
end
end
これらのメソッドにより、次のように読みやすく、コピー&ペーストしやすい出力が生成されます:
require 'pp' gem 'ParseTree' pp Gem.loaded_specs["ParseTree"].dependencies
[Gem::Dependency.new("RubyInline",
Gem::Requirement.new([">= 3.7.0"]),
:runtime),
Gem::Dependency.new("sexp_processor",
Gem::Requirement.new([">= 3.0.0"]),
:runtime),
Gem::Dependency.new("hoe", Gem::Requirement.new([">= 1.8.0"]), :development)]
0 件のコメント:
コメントを投稿