Memento memo.

Today I Learned.

Effective Ruby 第五章 メタプログラミング まとめ その2

Effective Ruby 第五章 メタプログラミング まとめ その1 - Memento memo. の続きです。

モンキーパッチの代わりとなるものを検討する

モンキーパッチとは

  • モンキーパッチとは、実行時にコアクラスを拡張したり挙動を書き換える(パッチを当てる)こと。Active Supportが有名。
  • 複数パッチが衝突すると大事故になるのでできるだけモンキーパッチは避けたい

方法1 module関数を使う

愚直にmoduleを定義&オブジェクトをextendする方法です。

module OnlySpace
  ONLY_SPACE_UNICODE_RE = %r/\A[[:space]]*\z/
  def self.only_space? (str)
    if str.ascii_only?
      !str.bytes.any? do |b|
        b != 32 && !b.between?(9, 13)
      end
    else
      ONLY_SPACE_UNICODE_RE === str
    end
  end

  def only_space?
    OnlySpace.only_space?(self)
  end
end

str1 = "   \r\n"
puts OnlySpace.only_space?(str1)
# OOPっぽくなくなる

str2 = "hello"
str2.extend(OnlySpace)
puts str2.only_space?
# extendしないと使えない

方法2 新しい別のクラスを作成する

Stringクラスの代わりにStringExtraクラスを作成します。 継承ではなく委譲を使ってStringライクなStringExtraを実装します。(実装省略)

方法3 Refinements機能を使う

Ruby 2.1から入ったRefinements機能を使います。 Refinementsでは、パッチの適用範囲がレキシカルスコープの中に限定されます。以下の例で String#loudFoo の外で呼ぶことはできません。

使い方はこんな感じになります。

module Loud
  refine String do
    def loud
      "#{self}!!!"
    end
  end
end

class Foo
  using(Loud)
  def initialize(str)
    puts str.loud
  end
end

Foo.new("wei")
#=> wei!!!

参考

エイリアスチェイニングで書き換えたメソッドを呼び出す

  • 既存のメソッドに新しい名前を与え、元のメソッド名でメソッドを再定義して最終的に元のメソッドを呼び出す。
  • エイリアス作成時にメソッド名がユニークになるよう注意
  • エイリアスチェイニングを取り消すメソッドも作成する

alias_methodについて

alias_method(new_name, original_name) でmethodにエイリアスを貼ることができます。

参考: ref.xaio.jp

sample

module LogMethod
  def log_method(method)
    orig = "#{method}_without_logging".to_sym
    
    if instance_methods.include?(orig)
      raise(NameError, "#{orig} isn't a unique name")
    end

    alias_method(orig, method)

    define_method(method) do |*args, &block|
      $stdout.puts("calling method '#{method}'")
      result = send(orig, *args, &block)
      $stdout.puts("'#{method}' returned #{result.inspect}")
      result
    end
  end

  #エイリアスチェイニングを取り消すメソッドも作成する
  def unlog_method(method)
    orig = "#{method}_without_logging".to_sym
    
    if !instance_methods.include?(orig)
      raise(NameError, "was #{orig} already removed?")
    end

    remove_method(method)
    alias_method(method, orig)
    remove_method(orig)
  end
end

Array.extend(LogMethod)
#=> Array
Array.log_method(:first)
#=> :first
[1,2,3].first
# calling method 'first'
'# first' returned 1
#=> 1
Array.unlog_method(:first)
#=> Array
irb(main):008:0> [1,2,3].first
#=> 1

Procの引数の個数の違いに対応できるようにする

  • Procオブジェクト生成には"強いProc"と"弱いProc"がある。 lambda? メソッドで識別可能。
  • 弱い(Weak)Proc: 引数の扱いが緩く、間違った個数の引数を与えてもエラーにならない。e.g. block
  • 強い(Strong)Proc: 通常メソッド呼び出しと同じで、引数の個数が違うとArgumentError例外が発生する。e.g. lambda
  • Proc#arityメソッドを使うと、Procオブジェクトが期待する引数の数がわかる。
  • Proc#arityメソッドで引数の個数の違いをうまく吸収させる。

モジュールのprependを使うときは慎重に考える

  • 継承階層は ancestors メソッドで確認可能
  • includeは継承階層においてレシーバの後にモジュールを挿入する。
  • prependは継承階層においてレシーバのにモジュールを挿入する。
  • prependよりもalias_methodを使った方が柔軟になる

メタプログラミング面白いですね。Refinementsで局所的にクラス拡張するのは結構気に入りました。

まだ メタプログラミングRuby 第2版 も読めていないのですが、健全なメタプロパワーを高めていきたいです。

Effective Ruby

Effective Ruby