«前の日記(2010年11月12日) 最新 次の日記(2010年11月14日)» 編集

Matzにっき


2010年11月13日 [長年日記]

_ [Ruby] RubyConf 2010 キーノート(2)

前回に続いて 未来(≒Ruby 2.0)の話を。

今回、紹介した「未来」の機能は以下の通り。

  • Traits
  • Method Combination
  • Keyword arguments
  • Namespaces

今まで話してきたことじゃん、と思うでしょうが、その通り。 違いは

  • これらの機能が単なるアイディアではなく、どのように実装すべきかほぼ見えている
  • 実装した機能を突っ込む場所(trunk)が明確になっている

点です。特に後者は大きい。

Traits

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関係が後から変化した場合の一貫性のない挙動
  • メソッドを後から修飾する(wrapする)方法が提供されない

ことです。

擬似的な多重継承である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の代わりに使うと、

  • 現在モジュールに定義されているメソッドを クラス/モジュールに注入する
  • mixされたモジュールは継承ラインに登場しない
  • メソッド名の重複は例外になる
  • 例外がイヤならモジュールを書き換える、 または重複したメソッドの名称を変更する
  • 定数を取り込むかどうかを指定できる。 デフォルトは取り込まない

という振舞いになります。これにより

  • 名称の重複はエラーになるので、見逃しがない
  • 名称変更ができるので、明示的に解消できる
  • あくまでも「現時点での状態の注入」なので、 継承ラインが変化した時の「おかしさ」がない。 問題は解決していないが、気分は良い(苦笑)

ということが実現できます。

たとえば以下のようなコードでは

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でこれを紹介したその日には中田さんが着手していて、 パッチは完成しているそうです。

ただ、各種プレゼンテーションでは説明しなかった以下の課題があり、 これらについては結論を出す必要があります。

  • mixされるモジュールが別のモジュールをincludeしていた場合にはどうなるか。 おそらくは例外になる。mixとincludeは混ぜるべきではない。
  • mixで別名を付けて問題解決、と読めるような言い方をしているが、 実際にはモジュール内部で名前を書き換える前のメソッドを読んでいる可能性がある。 それをどうするか。なにもしない(重複する方が悪い)とする考えもあるが、 それだとせっかく苦労してTraitsを導入しようとしているのが まったく無駄になるので、名前を書き換えたメソッド呼び出しを モジュールのメソッド定義実体から探し出してメソッドをコピーする という(Bertrand MayerのOOSCに記述されていたアイディア)を導入することを考える
  • インスタンス変数の名称重複を解決する手段がない。 これはサブクラスからは見えないインスタンス変数を導入し、 mix対象のクラスではそちらを使うこと推奨とするべきではないかと 考えています。1.9向けのパッチは既に書いてありますが、 プライベートなインスタンス変数の記法を @_fooをにするか、@__fooにするか、 はたまたまったく違うナニかを考えるのかが難しくて現状では放置されています。 mixが導入されたらより必要になるでしょうね。

Method Combination

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のようなものを 継承チェーンに置いて、そっちを先に検索するようにするんじゃないかなあ。

Keyword Arguments

引数、特にオプショナル引数がどんな働きをするのか忘れる人は私だけじゃないと思います。 たとえば、 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

後、「**」で辞書形式で受けとるとか、ちょっとした機能追加もありますが、 基本的にはこれだけ。

Namespace

技術的な詳細などについては同じ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度登場しないことを前提にしていたため断念


«前の日記(2010年11月12日) 最新 次の日記(2010年11月14日)» 編集