使いやすい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_count
Gem::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 件のコメント:
コメントを投稿