深読みしないRuby Refinements

ruby 2.1からの Refinements を使ってみようとして、思い通り通りに動かないことが多かったので、まとめた資料です。 仕様をまとめたものはRefinementsSpecにあるのですが、そこまで読まずに簡単に試してみたい場合にポイントとなるところをまとめます。

基本的な使い方

例として文字列のサイズを偽るRefinementsを使います。 これ以降の例でもusing Aが出てきますが、同じモジュールです。

Stringの長さが-1になっていれば、このRefinementsの影響を受けています。

module A
  refine String do
    def size
      -1
    end
  end
end

using A
'hoge'.size                     # => -1

usingを使うと、それ以降のスコープでリファインしたメソッドの挙動が変わります。

クラスやモジュールでスコープを切ることもできます。便利ですね。

class B
  using A
  def hoge
    'hoge'.size
  end
end

B.new.hoge                      # => -1
'hoge'.size                     # => 4

スコープについて

usingを使うとそれ以降の処理に影響します。

以下の例では、usingを使う前後で結果が変わっています。

'hoge'.size                     # => 4
using A
'hoge'.size                     # => -1

using以降のクラス定義も影響を受けます。 以下の例ではClass B内ではusingを仕様していないにもかかわらず影響を受けます。 場合によっては、あまり想定しない影響を起こすかもしれません。

using A

class B
  def hoge
    'hoge'.size
  end
end

B.new.hoge                      # => -1
'hoge'.size                     # => -1

モジュール内のクラス全てにリファインメントを影響させることもできます。 こちらは通常意図する使い方になると思います。

module D
  using A
  class B; end

  class C
    def fuga
      'fuga'.size
    end
  end
end

D::C.new.fuga                   # => -1

スコープについて2

クラス定義内でも順序を入れ替えるとusing前のメソッド定義は影響を受けません。 この例ではメソッド定義後にusingを使っているため、上書きされません。

# Bad example
class B
  def hoge
    'hoge'.size
  end
  using A
end

B.new.hoge                      # => 4

ややこしくなるので、通常は先頭でusingを呼ぶ方が良さそうです。

以下の例では、後からRefinementsの影響を入れるために、クラスを再オープンしてusingを使用しています。 しかし、元のクラスで定義されたメソッドはRefinementsの影響を受けませんので、この使い方は無意味です。

# This does NOT work
class B
  def hoge
    'hoge'.size
  end
end

class B
  using A
end

B.new.hoge                      # => 4

直接usingした範囲でしか影響しない

ここははまりどころだと思いますが、 Refinements の影響を受けるのは直接usingを使用されたクラスのみです。 そのクラスから呼ばれていても、別のクラスのメソッドを経由した場合は、元のメソッドが呼ばれます。

ややこしいのでサンプルで説明します。

class B
  def hoge
    'hoge'.size
  end
end

class C
  using A
  def fuga
    B.new.hoge
  end
end

C.new.fuga                      # => 4

class Cusingを使用しています。しかし実際にString#sizeを呼ぶのはclass Bです。 そのためclass Cでのusingの影響は受けず、元の長さを返します。 class Cでのusing Aで文字列の長さを変えたいと意図していますが、そのようには動作しません。

さらに別の例があります。こちらもはまりやすいパターンです。

class String
  def my_size
    size
  end
end

class B
  using A
  def hoge
    'hoge'.my_size
  end
end

B.new.hoge                      # => 4

Stringに別のメソッドmy_sizeを定義して、その中でsizeを呼んでいます。 my_sizeusingしたBから呼ばれるので、一見上書きしたsizeが呼ばれそうですが、String#my_sizeには影響していないので、呼ばれるのは元のsizeです。

まとめ

Refinementsはあくまでそのクラスから見たメソッドの見え方を変えるもので、他のクラスには一切影響しません。たとえメソッド呼び出しの契機となるクラスでusingを使っていても、他のクラスを経由した場合の動作は変わりません。*1

モンキーパッチとして使う場合、表側に近いクラスの挙動は変更しやすいですが、奥の方にあるクラス(内部向けのクラスなど)や多数のクラスから呼ばれるメソッドの挙動を変えるには工夫が必要そうです。

「限定されたスコープで動作する」というところがRefinmentsのポイントなので、スコープが不足していると思ったら、別の方法も検討しましょう。

*1:わかりやすくクラスと書いてますが、もちろん使い方によってはクラスだけではないです