モジュール内でクラス拡張はできない

class String
  def root_hello
    "root hello"
  end
end

module RootModule
  class String
    def root_module_hello
      "root module hello"
    end
  end
end

モジュール内で"".root_module_helloと使ってもNoMethodErrorとなります。これは""を使うと、拡張されていないトップレベルのStringが生成されます。String.new.root_module_helloとすばれ、拡張したメソッドを使うことができます。モジュール内でStringとクラス名を直接書いた場合は、モジュール内のStringが参照されます。

トップレベルのStringは文字列を生成するクラスですが、モジュール内で定義したStringはただの名前が同じの別クラスです。クラス拡張しているわけでもなく、この2つのStringは全く関係がありません。だから、モジュールの外から"".root_module_helloは使えません。モジュールはクラス拡張を局所的にすることができます。

クラス拡張したならコンストラクタは正常だ

下記のコードは上は同じ名前のStringを定義しているだけです。initializeを定義していないので、ArgumentErrorが起きています。下は親クラスを指定して継承して、initializeも受け継がれています。これは既存クラスを拡張しているわけではないので、トップレベルのStringとは別物です。

module Module1
  class String; end

  def self.create_instance
    String.new "test" # ArgumentError
  end
end

module Module2
  class String < ::String
  end

  def self.create_instance
    String.new "test"
  end

  def self.compare_string?
    String.equal?(::String) # false
  end
end

ダブロコロンを使った型判定

次はダブルコロンあり、なしでの型判定について見ていきます。

module NoExtend
  def self.compare_string?
    String === ::String # false
  end

  def self.compare_string2?
    String === "" # true
  end

  def self.compare_string3?
    ::String === "" # true
  end
end

module Extend
  class String; end

  def self.compare_string?
    String === ::String # false
  end

  def self.compare_string2?
    String === "" # false
  end

  def self.compare_string3?
    ::String === "" #true
  end
end

NoExtendExtendでも、String === ::Stringはfalseを返します。 mod === objは、 is_a?/kind_of?と同じです。オブジェクトobjmodクラスのインスタンスかを調べます。::StringStringのインスタンスではなくクラスです。だからfalseが返ります。モジュール内でクラス拡張した際は、型判定にはコロンをつけてトップレベルのクラスを使うようにした方がいいかもしれません。

2つのモジュールの違いは、String === ""の返り値です。これは前述した内容と同じです。別のStringを定義したせいで、Extendの中のStringはトップレベルのを参照しません。別のStringになっていることを次のコードで確認してみましょう。String.equal?(::String)の部分です。

クラス拡張をすると、モジュールの名前空間に新しいクラスが登録される

module NoExtend
  def self.equal_string?
    String.equal?(::String) # true
  end

  def self.equal_string2?
    String.equal?("") # false
  end

  def self.equal_string3?
    ::String.equal?("") # false
  end
end

module Extend
  class String; end

  def self.equal_string?
    String.equal?(::String) # false
  end

  def self.equal_string2?
    String.equal?("") # false
  end

  def self.equal_string3?
    ::String.equal?("") # false
  end
end

equal?==の別名です。同じオブジェクトかを判断しています。NoExtendの方ではtrueのままです。クラス拡張を行わない場合は、Stringのままでトップレベルのを参照できています。String.equal?("")::String.equal?("")はクラスとインスタンスが同じかを比較しているので、クラス拡張や名前空間は関係なしにfalseになります。

クラス拡張を局所的にするにはどうするか?

モジュール内で継承クラスを作るか、refineusingを使います。

module NormalUsingModule
  refine String do
    def normal_using_hello
      "normal using hello"
    end
  end
end

using NormalUsingModule

上記のusing以降から"".normal_using_helloが使えます。このファイルをrequireした場合は、再度usingを記述しないといけません。usingのスコープはファイル内に留められています。

さて、どちらの方法を使えばいいのか?自作のライブラリでしか使わないなら直接クラス拡張を、クラス拡張自体を色々な場所で使いまわしたいならusingを使うといいと思います。ただ、前者でも自作ライブラリの中で全体ではなく、さらに一部だけに影響を絞りたいならusingを使うといいでしょう。