LoginSignup
10
6

More than 5 years have passed since last update.

アラウンドエイリアスとメソッドラッパー(メタプログラミングRuby)

Last updated at Posted at 2018-04-28

アラウンドエイリアス

Rubyでは、メソッドにエイリアス(別名)をつけることができます。

sample.rb
class Sample
  def sample_method
    "abcdefg"
  end
  alias_method :old_sample, :sample_method
end

obj = Sample.new
puts obj.sample_method # => "abcdefg"
puts obj.new_method # => "abcdefg"

alias_methodメソッドでは、一番目のシンボルにエイリアスを設定し、二番目のシンボルには元のメソッドの名前を入力します。
上の例では、sample_methodの別名にnew_methodが設定されたため、
obj.sample_methodとobj.new_methodは同じメソッドを実行しています。

このエイリアスの実用的な使い方として、アラウンドエイリアスという手法が存在します。

sample.rb
class Sample
  def output; 10 * 10; end
  alias_method :old_output, :output
end

sample = Sample.new
puts sample.output # => 100

Sample.class_eval do
  def output; old_output * 10; end
end

puts sample.output # => 1000

アラウンドエイリアスとは、新しいメソッドで、古いメソッドを覆う(ラップする)ことです。
上記のコードの場合、エイリアスとして付与した名前を古いメソッドとして捉え、
元のメソッドの名前を新しいメソッドとして捉えます。

ん、逆なんじゃないの? みたいな感覚になるかもしれませんが、
エイリアスは、元のメソッドの、保存用の複製と捉えてみてください。
複製元のメソッドは更新用として存在し、その中のコードを変更するのです。

そのため、元のメソッドのコードをいじるわけですから、それを新しいメソッドであると捉えるのには、少し抵抗があるかもしれません。
しかし、中身は更新されるわけですから、名前は同じでも、元のメソッドではないのです。
元のメソッドの中身は新しく命名したエイリアスのメソッドの中にあります。

alias_method.rb
alias_method :old, :new
:old # => メソッドのエイリアス。この中に元のメソッド(new)の内容が複製される。
:new # => 元のメソッド。この中に新しいメソッドのコードを書く(複製しているからエイリアスメソッドを呼び出すことで、元のメソッドの内容を復元できる)。

このように、古いメソッドを新しいメソッドの中で呼ぶことをアラウンドエイリアス(古いメソッドであるエイリアスメソッドのアラウンドを新しいメソッドで囲っているので)と呼びます。

アラウンドエイリアスは、
新しく作り直すメソッドの中で、元のメソッドで得られる戻り値が必要な時
に使えます。

メタプログラミングRubyの例を載せると(第二版138ページ参照)、

sample.rb
class String
  alias_method :real_length, :length

  def length
    real_length > 5 ? "long" : "short"
  end
end

"War and Peace".length # => "long"
"War and Peace".real_length # => 13

ここで新しく作り直したlengthメソッドは、もともと、文字列の長さを返すメソッドでした。
しかし、上のコードでは、文字列の長さが5文字以上なら、"long"を、5文字以下なら"short"を返すメソッドに更新されています。

このメソッドを作成するには、もともとのlengthメソッドに備わっていた、文字列の長さを返すロジックが必要になってきます。
すでにalias_methodでもともとのlengthメソッドの中身をreal_lengthに複製しているので、lengthメソッドの中でreal_lengthを呼び出せば、それが文字列の長さを計算してくれます。
real_methodの戻り根を5以上かどうかを判断し、Trueなら"long"、Falseなら"short"を返すように(ここでは三項演算子を使っています)、lengthの中身を書き換えると、無事、「文字列の長さが5文字以上なら、"long"を、5文字以下なら"short"を返すメソッド」を定義することができます。

これらの作業を上のコードでは実践しているわけです。

メソッドラッパー

アラウンドエイリアスのように、メソッドでメソッドを包み込む技法のことを、
メソッドラッパーと呼びます。

メソッドラッパーには、アラウンドエイリアスのほか、refinements(改良化)と、prependメソッドを利用したものがあります。

refinements

まずはrefinementsについて説明します。

sample.rb
class Sample
  def a_method
    "original method"
  end
end

sample = Sample.new
puts sample.a_method # => "original method"

module RefineSample
  refine Sample do
    def a_method
      "refined method"
    end
  end
end

using RefineSample

puts sample.a_method # => "refined method"

refinementsは、すでに存在するクラスのメソッドを参照して、そのメソッドの中身を一時的に変更します。
上記のコードでは、既存のクラスSampleのa_methodメソッドを、
moduleの中で展開したrefineメソッドのブロックの中で書き換えています。

しかし、このままではa_methodメソッドの内容は更新されません。
このmoduleのrefineの使用宣言をしなければならないのです。

使用宣言は、

sample.rb
using ModuleName

で行えます。
この宣言以降にa_methodを呼び出すと、その内容は以前のものから変化しています。
refineの有効範囲は、usingを使って使用宣言箇所から、そのスコープの終わりまであります。

先ほどの例であれば、ファイルが終わるまでですが、
moduleやclassの中でusing使用宣言をすると、そのmoduleやclassの終わりまで、refinementsは有効です。

このrefinementsもメソッドラッパーとして利用できるのです。
メタプログラミングRubyの説明がわかりやすいので引用します(第二版140ページ参照)。

sample.rb
module StringRefinements
  refine String do
    def length
      super > 5 ? "long" : "short"
    end
  end
end

using StringRefinements

puts "War and Peace".length # => "long"

refineのブロックの中に、既存のlengthメソッドを記述し、新しいロジックを書き込みます。
このとき、メソッドのスコープ内でsuperを呼び出すと、もともとのlengthメソッドに備わっていたロジックもよみがえります。

つまり、上記のコードでは、
新しいlengthメソッドの中に古いlengthメソッドを、superという記述で呼び出し、新しいロジックの中に組み込んだ、
ということになります。

古いメソッドを新しいメソッドで覆っている(ラップしている)ので、これはメソッドラッパーです。

Module#prepend

さらにもう1つ、メソッドラッパーの性質をもつメソッドが存在します。
それはprependメソッドです。

prependメソッドはincludeメソッドの変化型です。
includeとは少し動きが異なります。

includeは、ご存知の通り、moduleを読み込むメソッドです。

sample.rb
module ModuleSample
  def calc
    1 + 1
  end
end

class Sample
  include ModuleSample
end

sample = Sample.new
puts sample.calc # => 2

includeしたmoduleは、この場合、インクルーダー(includerはincludeしたものの意味。したがってこの場合はSampleクラス)の上に挿入されます。
以下のコードを調べてみるとすぐにわかります。

sample.rb
# 先ほどの続き。
p Sample.ancestors # => [Sample, ModuleSample, Object, Kernel, BasicObject]

ancestors(先祖)メソッドは、Sampleが継承しているmoduleとclassを、一番最初のクラスであるBasicObjectまでさかのぼります。
早い話がレシーバーの先祖を若い順から参照することができるわけです。

includeすると、ancestorsメソッドによれば、Sampleの1つ上にModuleSampelモジュールが挿入されていることがわかります。
しかし、prependでは、これと挙動が異なってきます。

sample.rb
module ModuleSample
  def calc
    1 + 1
  end
end

class Sample
  prepend ModuleSample
end

sample = Sample.new
puts sample.calc # => 2

モジュールを参照して、計算結果を出力する点では、includeと同一の動きをします。
しかし、

sample.rb
# 先ほどの続き。
p Sample.ancestors # => [ModuleSample, Sample, Object, Kernel, BasicObject]

先祖たちの順番が変わってしまいました。

ここからわかるように、prependメソッドでモジュールを利用すると、そのモジュールはインクルーダーの下にやってくるのです。
includeメソッドを使うと、モジュールはインクルーダーの上にやってきていましたが、インクルーダーの下へやってくるprependの利用価値とはなんなのでしょうか?

それは、インクルーダーのメソッドをオーバーライドできる点にあります。

ModuleSampleモジュールがSampleクラスの上にあれば、ModuleSampleの中でSampleのメソッドを呼び出すことはできません。クラスもモジュールも、上のクラスやモジュールからしかメソッドを継承できないからです。
そう考えると、下にいるモジュールは、上にいるクラスのメソッドを参照できるはずです。

prependメソッドによって、インクルーダーであるクラスの下に挿入されたモジュールも、もちろんそのインクルーダーのメソッドを参照できるのです。

sample.rb
class Sample
  def calc
    10 + 10
  end
end

module ModuleSample
  def calc # => Sampleクラスのcalcメソッドを上書き。
    20 + 20
  end
end

Sample.class_eval do
  prepend ModuleSample # => モジュールをprependメソッドでインクルードする。
end

sample = Sample.new
puts sample.calc # => 40 (Sampleクラスのcalcメソッドではなく、ModuleSampleモジュールのcalcメソッドを返している)

p Sample.ancestors # => [ModuleSample, Sample, Object, Kernel, BasicObject]

prependメソッドは、このような動きを利用して、メソッドラッパーの効果を発揮します。
以下はメタプログラミングRubyから引用したコードです(第二版141ページ参照)。

sample.rb
# Stringクラスはデフォルトで存在するので書きません。

module ExplicitString
  def length
    super > 5 ? "long" : "short"
  end
end

String.class_eval do
  prepend ExplicitString
end

"War and Peace".length # => "long"

上記のコードでも、refinementsと同様に、新しいlengthメソッドの中で古いlengthメソッドの戻り値をsuperで呼び出しています。
つまり、メソッドラッパーなのです。

3つのメソッドラッパー

ここではメソッドラッパーを3種類紹介しました。

  1. アラウンドエイリアス
  2. refinementsラッパー
  3. prependラッパー

これらは、古いメソッドを上書きするときに、元のロジックを残しながら、新しく更新するロジックを定義し、また、その古いロジックを新しいロジックの中で再利用する手段を与えてくれています。

初学者からすると混乱してしまう要因の1つとなりえそうですが、コードを何度か書いてみると、徐々に実感していけると思います。

10
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6