RubyはJavaより難しい(と思う) - Railsの acts_as の話 -
Rubyはやはり難しいです。
まず記号が多い点があげられます(http://www.ruby-lang.org/ja/man/html/Ruby_A4C7BBC8A4EFA4ECA4EBB5ADB9E6A4CEB0D5CCA3.html#a.3a)。一般的には、記号の多さは可読性を落とす気がします。 そして、(Railsが顕著すぎるのかもしれませんが)メタプログラミングの多用があります。メタプログラミングはクールですが、一般的には分かりやすいコードとは言えません。 そして、個人的見解ですが、そもそも未だに動的型言語が正しいとは思えていません。変数の型を明示しないことが便利に働く場面があることは認めます。Cで言えば、プリプロセッサマクロやvoidポインタで多態的なコードを書くような場面です。しかし、多く見積もっても、型を明示しないことが便利なケースは全体の20%程度だと思います。20%は結構多く見積もっているつもりです。残り80%は、変数に型を明示した方が可読性が上がると思います。書く側にも読む側にも安心感が得られるからです。そしてコンパイルさえ通せば、想定している型と違うオブジェクトだったという、あまりに基本的なバグから(ほぼ)逃れられます。
ぼくがプログラミングで最も重視するのは、読み手のコストです。読み手のコストを減らすためには、書き手は相当のコストを背負っても良いと思っています。
と、Ruby大嫌い人間みたいですが、Rubyの話です。
Ruby on Railsのpluginのコードの話です。acts_as_listを例にコードを見てみます。コード例はv2.0.2ベースです。
init.rbは以下のようになっています。
$:.unshift "#{File.dirname(__FILE__)}/lib" require 'active_record/acts/list' ActiveRecord::Base.class_eval { include ActiveRecord::Acts::List }
これを呼び出す場所は調べていませんが、pluginsディレクトリ下にあるinit.rbをロードする処理がどこかにあるはずです。
最初の行はロードパス変数 $: (型はArray) に相対パスを挿入しています。次の行で相対パスのlist.rbファイルをロードします。list.rbファイルは後で見ます。 3行目で、既存クラスの ActiveRecord::Base にモジュールをincludeしています。class_evalに渡すブロック内の self はActiveRecord::Baseのクラスオブジェクトを参照しています。 ここは、次のように書いてもいいはずです。
class ActiveRecord::Base include ActiveRecord::Acts::List end
list.rbの骨子だけ抜き出すと次のようなコードになっています(コメントはごっそり削除)。
module ActiveRecord module Acts #:nodoc: module List #:nodoc: def self.included(base) base.extend(ClassMethods) end module ClassMethods def acts_as_list(options = {}) configuration = { :column => "position", :scope => "1 = 1" } configuration.update(options) if options.is_a?(Hash) configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/ if configuration[:scope].is_a?(Symbol) scope_condition_method = %( def scope_condition if #{configuration[:scope].to_s}.nil? "#{configuration[:scope].to_s} IS NULL" else "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}" end end ) else scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end" end class_eval <<-EOV include ActiveRecord::Acts::List::InstanceMethods def acts_as_list_class ::#{self.name} end def position_column '#{configuration[:column]}' end #{scope_condition_method} before_destroy :remove_from_list before_create :add_to_list_bottom EOV end end module InstanceMethods def insert_at(position = 1) insert_at_position(position) end 多くのメソッド...(略) end end end end
このコードをぱっと見て、どこがどんなタイミングで呼ばれるかすぐに分かればすごいです。少なくともぼくは分かりません。
Rubyの考え方として、上のinit.rbからrequireされたタイミングで、list.rbのコードがすべて評価される(実行される)と考えてください。例えばもし、module Listの行の下に print "foobar" と書いてあれば、requireされた直後に foobar が出力されます。実際のlist.rbの中にはdefしかないので、defの中身はメソッド定義として評価され、中身が実行されるのは各メソッドが呼ばれた時になります。
最初のポイントは、self.included がinit.rbの3行目のincludeのタイミングで呼ばれることです。includedメソッドの動作は次のようにirbで確認できます。
irb> module M def self.included(m) p "M included" end end irb> class C include M end "M included"
includedメソッドの中身は base.extend(ClassMethods) です。この base は ActiveRecord::Base のクラスオブジェクトを参照しています。extendはオブジェクトにモジュールを加えるメソッドです。この場合、クラスオブジェクトにモジュールをextendしているので、意味的には次と同じ処理になります。
class ActiveRecord::Base class << self include ClassMethods end end
ClassMethodsの中にはacts_as_listがあります。これは次のように呼ばれることを想定しています(Booksは開発者が作るクラス)。
class Books < ActiveRecord::Base acts_as_list end
上のように呼ばれると、acts_as_list内のclass_evalが実行されます。この時、class_eval内の self は Books を参照しています。意味的には次のようなincludeと同じになります。
class Books include ActiveRecord::Acts::List::InstanceMethods end
これでめでたくBooksクラスのオブジェクトは、InstanceMethodsモジュールのメソッドを使えるようになりました。
- Category(s)
- カテゴリなし
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/inoue/ruby-is-hard/tbping