bar_1

contents_map

2017年11月19日日曜日

[Ruby]モジュールに継承/派生っぽいことをさせてみる試み

組み込みライブラリもしくは既存のクラスclassやモジュールmoduleを, 特定の用途向けに一部だけ改変したいことがある. その際, できればモンキーパッチはしたくない.
このような場合, クラスならば class DerivedKlass < OriginalKlass として継承を使える. 派生クラスを作ることで, 元のクラスのコピーを作り, それを改変すればいい.
ではモジュールの場合は, どうしたらよいだろう? ここでは, Mathを取り上げて考えてみる.
最初に, include/extend/refineという既存の方法の検討をおこない問題点を明らかにする. 次に, これらの問題点を解決する Module#module_compose の導入を提案する.


include (Module#include) を使った場合

Module#include は, 特定のモジュール/クラスに対して, 指定したモジュールの定義 (メソッド, 定数) を受け継ぐ. 多重継承の代わりに用いられることを意図している.
これを使った場合:
module MyMath
  include Math
  def some_function; ...; end
end
MyMath.sin(0.5)
とすることは……できない (NoMethodError: undefined methodsin’ for MyMath:Module`).
Mathにあった各種モジュール関数は, MyMathの外部から使うことは出来ない. Module#prepend を使った場合も同様である.

extend (Object#extend) を使った場合

Object#extend は, 特定のインスタンスに対して, 指定したモジュールで拡張する.
module MyMath
  extend Math
  def some_function; ...; end
end
こうしたときも MyMath.sin() のような使い方はできない (NoMethodError: private methodsin’ called for MyMath:Module`): モジュール関数として用意される2つのメソッドのうちのひとつのインスタンスメソッドは, privateだからだ.

refine-using (Module#refine)

refineならどうか? 目的には, 合いそうな気がするが…
module RefineMath
  refine Math do
    def some_function; 1; end
  end
end
# =>TypeError: wrong argument type Module (expected Class)
うぅ……. そもそもrefineはクラスを扱う のだった. また仮に出来たとしてもincludeのケースと同じことになるだろう. using をしたモジュール/クラスの外側からは使えないからだ.

Module#module_compose の導入

「なんらかのオブジェクトをモジュールで拡張したい」のではなく, 既存のモジュールそのものを拡張したい場合は, どうしたらいいだろう(モンキーパッチはしない前提で). もともとのモジュールの使い勝手を変えずに, かつそのモジュールの機能を新たなモジュールにコピーする方法はないだろうか? モジュールも, クラスのように派生できたら…….
具体的には:
  • 元のモジュール (たとえばMath) のクラスメソッドは, 新モジュールのクラスメソッドへと
  • 元のモジュールのインスタンスメソッドは, 新モジュールのインスタンスメソッドへと
マッピングするような仕組みがあればよい.
これらのことを実現するために, 以下に示す Module#module_compose を導入する:
#
# module macro Module#module_compose
#

# copy module(s) into a module.
# ==== Args
# mods :: module(s)
# ==== Return
# self
# ==== Usage
# The usage is same as `include'.
#
# Ex.
#   module My::Math
#     module_compose Math
#     module_function
#     def foo; ...; end
#     :
#   end
#
class Module
  public
  def module_compose( *mods )

    #
    mods.each do |mod|
      # store ancestors info.
      include mod

      # define delegation methods of mod.methods.
      methods_class = mod.singleton_methods
      # $stderr.puts methods_class

      methods_class.each do |cmeth|
        self.singleton_class.send( :define_method, cmeth ) do |*args|
          mod.send( cmeth, *args )
        end
      end

      # instance methods (public/protected/private) of mod can be used
      # because we did `include mode`.
      #
    end

    self
  end

end

#
処理の中身としては, 新モジュール内で, 拡張の対象となる元のモジュールをincludeをし, 元のモジュールのクラスメソッドへのチェーンを定義する. だけだ.
以下のように使う. 元のモジュールと同じ感覚で使えるモジュールMyMathを作れるのだ:
require 'module_compose'
# => true
module MyMath
  module_compose Math
  def my_method; 1; end
end
# => :some_method

MyMath.sin(0.5)
# => 0.479425538604203
include MyMath
# => Object
sin(0.5)
# => 0.479425538604203
my_method
# => 1
Mathモジュールから派生(module_compose)したMyMathで, 継承元のクラスメソッドもインスタンスメソッドも使える.

まとめ

  • Rubyでモジュールを継承/派生させる試みとして, 新たに Module#module_compose を導入した
  • Module#module_compose により, モンキーパッチなしで既存モジュールを変更することが可能となった

今後の課題など

  • 変数については考慮していない
  • forwardable, delegate の検討は行っていない
  • Rubyの想定するモジュールの使用法から逸脱している可能性はある. MatzによるMix-inの解説, 2005. 一般に, クラスメソッドやインスタンスメソッドの定義の自動化には, ClassMethods, InstanceMethodsという名のサブモジュールを用意しておき, フックメソッドを使ってextendやincludeする方式(self.included(base) イディオム)が, よく使われている

0 件のコメント:

コメントを投稿

何かありましたら、どうぞ: