教えながら学ぶRuby: 言語を拡張したくなる衝動に関して
2007.10.11
少しMBAの勉強の方が一段落したので、今日はRubyの勉強。「Ruby本」にサンプルとして掲載されているチャットのプログラムを色々な技巧を使って「どこまで美しくできるか」を試みるのが今日の課題。そこで悩んでしまったのが、「やたらと言語を拡張したくなる衝動」を押さえるべきかどうか。Rubyの場合、すべてのものがオブジェクトで、かつ、すでに存在するクラスにメソッドを自由に追加できるので、FixNumだとかNilClassなど基本的なクラスの再定義をすることにより、あたかも言語を拡張しているような効果を生むことが可能なのだ。
今日書いていて気に入らなかったのは下のコードの太字の部分。
while(!@err)
r,w,e = select(@socks)
next if r.nil?
r.each { |sock|
case sock
...
}
}
これは、selectから返されたrがnilだった場合に、それに続くr.eachでエラーにならないようにするための処理だが(注:実際にはタイムアウトのパラメターを与えていないのでnilが返るはずはないのだが、とりあえずそれは無視していただきたい)、この一行がどうにも読みにくくしている。
そこで私がすかさず思ったのは、「NilClassにeachメソッドを加えれば良いじゃん」である。
class NilClass
def each
end
end
「nilのすべての要素に対して何かをする=何もしない」なので、論理的にも正しい。こうしておけば、上のプログラムは、
while(!@err)
r,w,e = select(@socks)
r.each { |sock|
case sock
...
}
}
とすることができ、ずっと読みやすくなる。
Smalltalkもそうであったが、Rubyを書いていると、こんな風にシステムで提供されているクラスを再定義したいという衝動に駆られる。こんな衝動に、ちまたのRubyistたちはどう対処しているのだろう?
上の例のように、システムクラスを拡張することにより、(その拡張を理解した)自分自身にとってのコードの可読性を上げることは可能だ。しかし、複数の人間が関わるプロジェクトでそれぞれのエンジニアが勝手にシステムクラスの再定義を初めてしまっては支離滅裂になってしまう。かといって、いちいち相談していては効率が悪い。結局のところは、Railsのように一人とか二人とかいったごく少数の人がフレームワーク構築の一環としてシステムクラスに手を入れ、他の人たちはその上に乗っかるようにプログラムを作る、というスタイルが現実的なのだろうか 。増井さんが指摘したように、確かにこれがRailsを使う大きなメリットの一つ、なのかも知れない。
こういうときはKagemushaを使うといいです。
http://kagemusha.rubyforge.org
これを使うと、NilClassなどの挙動の修正を
ブロック内に閉じ込めることができます。
Posted by: ujihisa | 2007.10.11 at 23:37
Rubyが大規模開発に向かないっていうのは、まさにこの辺のことですね。
コレを使わないと、Rubyを生かした生産性向上が得られないけど、コレを使うためには全体をちゃんと把握してないといけない。
RailsのActiveSupportが導入されると、Ruby自体のバージョンが上がったような感じになりますよね。なので、この手の書き換えは言語のバージョン問題と同じような感じになり、アプリを開発するラインとは別に、この管理をするチームが必要になると思います。
あと、こういう変更は一カ所の修正が及ぼす範囲が非常に広くなりがちなので、UnitTestなどの重要性がものすごく高くなりますね。
Posted by: masuidrive | 2007.10.12 at 01:17
NullObjectを組み込みの型に使えるということだけみると便利ですね。でも、かなり危険なので使い方には注意が必要そうで。
Posted by: K2 | 2007.10.12 at 06:28
はじめまして。本題の趣旨からは外れるかも知れませんが、r.eachでのエラーを避けるだけでしたらNilClassの拡張を考えるよりも
while(!@err)
r,w,e = select(@socks)
r.each { |sock|
case sock
...
} if r
}
の方が簡潔かと思います(可読性も個人的にはこちらの方が良いと思います)。Rubyにおけるシステムクラスを拡張する必要性が本当にある場合というのは極めて稀だと思いますので、生産性向上のみを目的とした拡張は熟考した上で行なうべき、ということではないでしょうか。
またちょっと疑問に思う箇所があったので質問させて頂きたく思います。
>「nilのすべての要素に対して何かをする=何もしない」なので、論理的にも正しい。
における「nilのすべての要素」とは具体的に何を指しているのでしょうか。
Posted by: keita | 2007.10.12 at 11:03
keitaさん、コメントありがとうございます。確かにシステムクラスの拡張には慎重になるべきだと思います。いただいたコードは、可読性もあるしバランスの良いコードですね。このエントリーの目的は、まさにこういうご指摘をいただくためのものだったこともあり、感謝・感謝です。
ちなみに、
>「nilのすべての要素」とは具体的に何を指しているのでしょうか。
ですが、これは少し禅問答のようなものですが、宗教用語で言うところの「空(くう)」には実際何も含まれていないので、「すべての要素に対してブロックを実行する」というeachをnilに食わせても「何もしない」というのがロジックとしては正しい、というのがここで言いたかったことです。すでにLispのエキスパートの方から、「Lispにおけるnilは()なので、それに何をしても何もしない」というご指摘もいただいており、私が指摘したロジックもあながち的外れではないかと。
Posted by: satoshi | 2007.10.12 at 11:16
お返事ありがとう御座います。「nilのすべての要素」について理解致しました。しかし ruby の意味論を考えますと、以下の点から nil を lisp のそれと同様に見做すのは困難であろうと思います。
・ruby においては nil と空配列 [] が区別されている
・nil と false もまた区別されている
ruby-list:37058 あたりを読むと ruby の nil は lisp の影響を受けているのだろうと思いますので、何故現在こうなったのかは不思議なところですよね。私の直観的には nil#each を定義することは不自然なのですが、でも"nil"という名前なのだから概念上何らかの空である状態を表しているのに違いないだろうと思いますので、正直よく分からないところです。しかし実際、
class NilClass
include Enumerable
def each
end
end
とすると、nil と [] の振舞いは極めて類似しますから、ある種の信念において nil#each のように言語を拡張することは妥当かも知れないわけですね。大変参考になりました、ありがとうございます。
Posted by: keita | 2007.10.12 at 12:56
nil.to_a #=>[]
nilにto_aが定義されているので一応こういうこともできます。
while(!@err)
r,w,e = select(@socks)
r.to_a.each { |sock|
case sock
...
}
}
Posted by: laplace | 2007.10.13 at 15:38
eachといわずnilは何を呼んでも何も起らない仕様の方がオブジェクト指向では便利だから、とかいう説明の方が良かったかもしれません^^。Objective-Cではまさにこの動作ですね。
Posted by: かわうそ | 2007.10.14 at 02:50
nilが「空である」という考え方が出来るのに、
nilと空配列 [] が何故分けられているの私なりの考察ですが、
bool型のtrue false の関係に、 数値でいう!0 0がありますよね。
同じように、空配列は配列におけるfalseや0に相当するのではないのかなと思っています。
NULLはSQLではNULLに空だけでなく、不明や不適合という意味があるんですよね。
その場合のtrue, false, NULLというのは常に別物で、
trueの状態かfalseの状態か判らない時にNULLが登場します。(*違う時もあります)
そのため、明確な空の配列とnilも区別されるのではないかと考えてます。
Posted by: haru666 | 2007.10.14 at 23:14
はじめまして。
主旨とずれますが、nil と [] について思い当たったことです。
データベースにおいては、文字列型の値について
値が何もない → null
長さ0の文字列 → ""
のふたつの区別する場合があります。
nil と [] も同じような関係ではないでしょうか。
値が何ものでもないことを示すのに nil
要素数が0個の集合(空集合)を示すのに [] を使用するという感じです。
だから、空集合 [] に対しては、[].each {|i| p i} が有効です。
なお、nil に対して空集合のイメージを持たれている方は、
nil.each や 「nilのすべての要素に対して何かをする=何もしない」の論理を
自然と理解することができるのでは、と思います。
ところで、そのオブジェクトが集合か否かを判定する isset という関数があった場合
isset(nil)は、true か、false かどちらを返すべきでしょうか。
Posted by: 田辺 | 2007.10.15 at 09:41