前回に続いて 未来(≒Ruby 2.0)の話を。
今回、紹介した「未来」の機能は以下の通り。
今まで話してきたことじゃん、と思うでしょうが、その通り。 違いは
点です。特に後者は大きい。
Traitsの定義は
a trait is a collection of methods, used as a "simple conceptual model for structuring object oriented programs".
from Wikipedia (en)
ということで、モジュールとほぼ同じようなものです。 実際、今回導入するTraitsは言語要素の実体としてはモジュールを利用します。
ただ、モジュールの機能を取り込むのにincludeではない 別のやり方(mix)を導入することによって、includeが持ついくつかの問題を解消しよう というものです。includeの方が便利なこともあるので、includeもなくなりません。
includeの問題は
ことです。
擬似的な多重継承であるincludeは、 includeされたモジュールが継承ライン(ancestors)に含まれるようになります。 この時、状況によっては予測困難なことが発生します。
ひとつはincludeされた複数のモジュールで同名のメソッドが定義されていた場合、 その重複が意図されたもの(override)か、偶然か(conflict)か、 区別する手段がないところです(名称重複問題)。
もうひとつは、いくつかの状況で継承ラインに並ぶモジュールの順序が予測しがたい (ので、メソッド名の重複時にどれが優先になるのか直感的でない)ことです。
module American attr_accessor :address end module Japanese attr_accessor :address end class JapaneseAmerican include American include Japanese end JapaneseAmerican.new.address # which address? p JapaneseAmerican.ancestors # => [JapaneseAmerican, Japanese, American, Object, Kernel]
この例ではaddressという属性(メソッド)がAmericanとJapaneseの間で 重複していますが、これが意図的な重複なのか偶然かは言語にはわかりません。 継承ラインの順にしたがってメソッドを呼び出すだけです。
実際にはJapaneseモジュールが優先されてそのaddressメソッドが呼ばれるのですが、 ひとめでそれが分かるのは、だいぶ「訓練されたRubyist」です。
現在のRubyでは、includeされた時、 「スーパークラスですでにそのモジュールがincludeされていた時には 二重にincludeしない」という挙動になっています*1。ですから、 スーパークラスでincludeされていることに気がつかなかった場合、 includeしても継承ラインのその場所にモジュールが登場しなかった ということが起こりえます。
それから、モジュールが既にincludeされてから、 そのモジュールに対してincludeを行った場合、 既に存在するクラスの継承ラインには新たにincludeされるようになったモジュールは含まれません。 つまり、includeのタイミングによって継承ラインへの反映のされ方が異なるわけです。 ちょっと気持ち悪いです。
これらを(ある程度)解決する手段がmixメソッドです。
mixメソッドをincludeの代わりに使うと、
という振舞いになります。これにより
ということが実現できます。
たとえば以下のようなコードでは
module American attr_accessor :address end module Japanese attr_accessor :address end class JapaneseAmerican mix American mix Japanese # => address conflict! end
addressメソッドが重なっているからmixできません。 無事mixさせるためには名称衝突を明示的に回避します。
class JapaneseAmerican mix American, :address => :us_address mix Japanese, :address => :jp_address end
これで、addressという名前による重複はなくなりました。
なぜ、includeにオプションをつけるのではなく、 新しいメソッドを導入して言語をより複雑にするかというと、 個人的にmixの挙動の方が望ましいと思っていて、 ユーザーをそちらに誘導するためには、より短い名前の方が望ましいと思ったからです。
Traitsを実現するmixメソッドの実装ですが、 RubyKaigiでこれを紹介したその日には中田さんが着手していて、 パッチは完成しているそうです。
ただ、各種プレゼンテーションでは説明しなかった以下の課題があり、 これらについては結論を出す必要があります。
RubyKaigiではmixの一部として導入する話をしていたMethod Combinationだが、 mixでいちいち「どのメソッドをラップするか」とか指定するのが以上にめんどくさいことに 後で気がつきました。ので、分離。
今回の案はprependというメソッドを導入すること。「include、mixに続いて またもうひとつ?」という声が聞こえてきそうだが、私もそう思います。でも必要なのよ。
prependはそのモジュールが提供する機能を、現在のクラス/モジュールの「前」に 追加する機能。
module Foo def foo p :before super p :after end end class Bar def foo p :foo end prepend Foo end Bar.new.foo # :before, :foo, :after
とように使う。prependしたモジュールFooで定義されたfooメソッドが、 prepend先のメソッドfooをラップしているのが分かるでしょうか。
prependメソッドは、RailsコミッタでもあるYehuda Katzの提案で、 これがあればRailsのalias_method_chainを撲滅できる、と息巻いていた。 私もそう思う。
具体的な実装はまだないんだけど、たぶんT_ICLASSのようなものを 継承チェーンに置いて、そっちを先に検索するようにするんじゃないかなあ。
引数、特にオプショナル引数がどんな働きをするのか忘れる人は私だけじゃないと思います。 たとえば、 public_instance_methods メソッドはオプショナル引数を受け付けるのだけど、 それが「オプショナル引数を付けると、それが真であった時にスーパークラスのメソッドを含む」のか、 それとも逆かというのは私でもいつも忘れてしまいます。正解はfalseを付けた時に含まない。
これをたとえば
aClass.public_instance_methods(include_super: false)
と書けたら、ずっと覚えやすくなるというものです。
Rubyのキーワード引数は、1.9で追加されたシンボル記法のハッシュが 引数リストの末尾に付いているだけです。
2.0で新たに追加されたのは、メソッド定義側でこれを簡単に受け取れる記法です。
例としてはこんな感じ。
呼び出し側
1.step(by: 2, to: 20) do |i| p i end
呼び出され側
def step(by: step, to: limit) ... end
後、「**」で辞書形式で受けとるとか、ちょっとした機能追加もありますが、 基本的にはこれだけ。
技術的な詳細などについては同じRubyConfで前田(修吾)くんが発表したスライドを見てもらった方が良いと思います。
Rubyではopen classといって既存のクラスの定義を書き換えちゃうことができる。 メソッドの追加も自由自在だ。このように既存のクラスに「パッチ」を当てちゃう技法のことを 「Monkey Patching」と呼ぶことも多い。
これは「ゲリラ・パッチング」→「ゴリラ・パッチング」→「モンキー・パッチング」と 変化して生まれた用語なんだって。 まあ、Rubyはクラスなんてものは変化するもんじゃないって「硬い」言語よりも 大きな自由を提供してることは確かだよね。 DHHは今回のRubyConfのキーノートで「今後はMonkey PatchingじゃなくてFreedom Patchingと呼ぼう」と 叫んでた。メル・ギブソンの『ブレーヴハート』を引用しつつ。「ふりーだーーむ」。
まあ、フリーダムなのは素晴らしいことなんだけど、影響力が大きすぎるというのもまた事実。 やろうと思えば整数のプラスメソッドを書き換えて、1+2 = 42 のような変更だってできちゃうから。 でも、大抵のプログラムは副作用でまともに動作しなくなるよね。
で、問題はこのような変更の影響の範囲がグローバル(プログラム全体)であることで、 仮にこのような修正をなんらかの「スコープ」に閉じ込めることができたなら、 もっと安全に、もっと安心して「フリーダム・パッチング」を活用できる、はず。
そのような「スコープ」のために、昔からClassboxとかSelector Namespaceとかが提案されてきた のですが、今回、前田くんが実装したのはSelector Namespaceの一種であるRefinment。
たとえば、以下のようなプログラムがあったとします。っていうか、あります。
class Integer def /(other) return quo(other) end end p 1/2 # => (1/2)
これは割り算演算子(/)を再定義して、整除ではなく結果を有理数にしようもので、 標準添付ライブラリの mathn の本質部分です。 しかし、整数の割り算の結果が整数であることを期待しているコードは当然存在するわけで、 そのようなコードは上のような変更で破綻する可能性があります。
そこで今回導入しようというのがrefinmentです(呼び名は変わるかもしれません)。 文法としては以下のようになります。
module MathN refine Integer do def /(other) return quo(other) end end p 1/2 # => (1/2) end p 1/2 # => 0
Refinementの単位としてはモジュールを使います。またモジュールです。大活躍ですね。
モジュールの中では既存のクラスをrefineできます。 refineの中で定義された修正はそのrefinment(ネームスペース)の中でだけ有効です。 ですから、MathNモジュールの中では 1/2 は有理数の (1/2) であり、 その外側では今まで通り整除になっています。有効範囲はレキシカルであり、 Refinmentはブロックの外側には影響を与えません。
Classboxとの違いは、そこを通じて呼び出された先(レキシカルスコープの外)に 「置き換え」が影響を与えるかで、いろいろ検討した結果、 多くのプログラミング言語がダイナミックスコープをあきらめたのと同様の理由で 「置き換え」はレキシカルになるべきだとの結論を出しました。
モジュールとして実現されたネームスペースを使うには usingメソッドを使います。 こんな感じ。
module Rationalize using MathN p 1/2 # => (1/2) end p 1/2 # => 0
これでRationalizeモジュールの中ではMathNが提供するRefinementが利用できます。
さらに、今までメソッドの中でメソッドをネストして定義した場合、 そのメソッドはクラスに直接定義されてあんまり意味ないじゃん、みたいな状態になっていたのですが、 今後はそのメソッドの範囲内でだけ有効なRefinementにネストの内側のメソッドが定義されるので、 完全にプライベートなメソッドとして使うことができます。
class Foo def foo def bar ... end bar # 呼べる end def quux bar # 呼べない end end
この変更はかなり大規模かつ複雑なものですが、前田くんのところでは実際に動作しています。 早く trunk に突っ込みたいものです。しかし、NaCl取締役の激務をこなしつつ、 こんなスーパーなパッチを作っちゃう前田くんに拍手。
歴史編で見てきた通り、ずっと昔からキーワード引数などについて話してきましたが、 とうとう現実になりそうです。長かった。
*1 MacRubyでは違うらしい。Ruby 1.9でそのような変更をしたかったが、YARVが継承ラインに同じモジュールが2度登場しないことを前提にしていたため断念