読者です 読者をやめる 読者になる 読者になる

クラス内で使いたい関数を明示的にincludeできるようにする

rubyで次のようなコードはわりとよくあると思います。

# some/utils.rb
module Utils
  module_function

  def html_escape str
    # ...
  end

  def html_unescape str
    # ...
  end
end

# some/more_utils
module MoreUtils
  module_function

  def some_func
  end
end

# some/foo.rb
class Foo
  include Utils
  include MoreUtils

  def initialize
    html_escape 'html'
  end
end

自分はこのコードが嫌な点が3つあって、

  • html_escapeがどこで定義されているかsome/foo.rbを見ただけではわからない
  • Fooクラスにはhtml_unescapeは不要なのにFooの中から呼べてしまい、しかもpublic_instance_methodになってしまう。
  • Fooクラスにもescapeというメソッドが他にも存在しているので別名にしたい

というようなことが少し気にかかります。

これを気にするのであれば、明示的にモジュール名を書けばよいという話もありますが、あまりにも頻度が多いとそれはそれでコードが読みにくくなってしまうのがなんともといった感じです。

そこで、ここをより明示的に書けるようにするにはどうしたらいいんだろうということで書いてみました。

http://gist.github.com/77523

module FunctionExporter
  def export context, *args
    parent_mod = self
    
    if args.size == 1 && args.first.kind_of?(Hash)
      args.first.each do |original_name, want_name|
        context.module_eval %{
          want_name = "#{want_name}"
          define_method want_name do |*params|
            #{parent_mod}.__send__ "#{original_name}", *params
          end
    
          if want_name =~ /^_/
            private want_name
          end
        }
      end
    else
      args.each do |func|
        context.module_eval %{
          func = "#{func}" 
          define_method func  do |*params|
            #{parent_mod}.__send__ func, *params
          end
        }
      end
    end
  end
end   
      
module FunctionImporter
  def import_function mod, *funcs
    mod.export self, *funcs
  end
end   

if $0 == __FILE__
  # 対象となるようなモジュールはmodule_functionで定義してあるのが前提
  module Utils
    extend FunctionExporter

    module_function
   
    def escape str
      str
    end
   
    def unescape str
      str
    end
  end

  # 一番単純な使い方。第2引数以降にFooの中で使いたいメソッドを並べる
  class Foo
    Utils.export self, :escape, :unescape

    def initialize
      p escape('hoge')
    end
  end

  # 上記の例と同様。selfを渡すのがちょっとカッコわるいのでシンタックスシュガーとしてFunctionImporterを使ってるだけ。
  class Bar
    extend FunctionImporter
    import_function Utils, :escape, :unescape

    def initialize
      p escape('hoge')
    end
  end

  # Hashで渡すと、keyのメソッドをvalueの名前として使えるようになる
  # _で始まるメソッドはprivateメソッドになる
  class Baz
    Utils.export self, {
      :escape => :_escape,
      :unescape => :my_unescape
    }

    def initialize
      p _escape('hoge')
    end
  end

  Bar.new
  begin
    Baz.new._escape('hoge')
  rescue Exception => e
    warn e
  end
  Baz.new.my_unescape('hoge')
end

ついmodule_evalでやってしまいましたが、こういうgemないかなぁと思って検索してみたらmodule-importというgemがみつかりました。

こちらは、

  • importがKernel空間に直接定義されているということと、
  • 別名で使えるようにするなどの機能がない ( githubのtrunkなどをみると別名で書ける機能もあるっぽい )

といった点が個人的には不満ですが、元のモジュールをdupして不要なメソッドをundefしまくってからincludeする実装になっているので、自分の手法に比べ、

  • 最終的にはincludeで入れているので元モジュールでmodule_functionしておく必要がない
  • 元モジュールで定義されていないメソッドがあればimport時に例外を出してくれる
  • Fooクラス内でのメソッド呼び出しのコストがないので実行時では速そう

といったあたりが優れているなぁと思いました。

探してみれば、過去に散々議論されてるネタなような気もします。

追記:

コードを一通り書き直してgemにしました。

gem install function_importerでインストールできます。

使い方などは特に変わってはいないですが、ドキュメントなどはこちらを見てください。 https://github.com/walf443/function_importer/tree