メタプログラミングとは
言語要素を実行時に操作するコードを記述すること
言語要素
- classやmethod、moduleなど、エディタ上で定義したままのプログラムの構成要素
- 通常、プログラムの実行時には言語要素は実体を失っているため、定義を変更することはできない
ただし、Rubyを除いては
コードを書くコードを書く
インスタンスメソッドを動的に生成する
# frozen_string_literal: true
class Sample
def initialize(method_list)
method_list.each do |mathod_name|
Sample.define_new_method(mathod_name.to_sym)
end
end
def self.define_new_method(method_name)
define_method(method_name) do
p method_name.to_s.upcase
end
end
end
mathod_list = %w[
hoge
fuga
]
sample = Sample.new(mathod_list)
# Sampleクラスには定義されていないhogeというメソッドを呼び出している
sample.hoge
スクリプトの一番下で、Sampleクラスには定義されていないhogeというメソッドが呼び出されている。ただ、このスクリプトは問題なく実行できる。
実行結果は以下の通り。
sampleオブジェクトがhogeメソッドを持つまでの流れた以下の通り
- Sampleというクラスはmethod_listという変数を受け取ってインスタンス化されている。
- initializeメソッドでは、method_listの中身が一つずつdefine_new_methodというSampleクラスのクラスメソッドに渡されている。
- define_new_methodでは組み込みのdefine_methodをブロック付きで呼び出している
- ブロックでは受け取ったmethod_nameを大文字にして出力している
問題は、define_methodで、このメソッドは第一引数にSymbolかStringを受け取り、インスタンスメソッドを定義することができる。define_methodのリファレンス
そのため、method_listの要素として渡された文字列はsampleオブジェクトのメソッドとして呼び出すことができるようになる。
エラーを捕捉してメソッドを生成する
# frozen_string_literal: true
class BasicObject
def method_missing(method_name)
BasicObject.define_missing_method(method_name.to_sym)
BasicObject.send(method_name.to_sym)
end
def self.define_missing_method(name)
define_method(name) do
print("\e[31m#{name}は定義されていませんが、エラーにはなりません\n")
end
end
end
class Sample
end
sample = Sample.new
sample.hoge
ひとつ前のスクリプトと同様に、sampleオブジェクトには定義されていない、hogeというメソッドを呼び出している。ただ、エラーにはならない。
Sampleクラスの継承のルートにはRubyの組み込みクラスであるBasicObjectクラスが存在する。Rubyの場合、組み込みのクラスであっても、その定義を書き換えることができる。この方法をオープンクラスと呼ぶ。
通常、定義されていないメソッドの呼び出しを行うと、以下のようなエラーになる。
この際、BasicObjectのmethod_missingメソッドが呼び出されるのだが、これをオーバーライドして、受け取った名前のメソッドをその場で生成しているのだ。
ただ、この方法は非常に危険だ。
class BasicObject
def method_missing(name)
name.undefined_method_exec
end
end
class Sample
end
sample = Sample.new
sample.hoge
このコードでは、BasicObjectクラスのインスタンスメソッド、method_missingの中でさらにundefined_method_execという定義されていないメソッドの呼び出しを行っている。
undefined_method_execメソッドが呼び出されると、当然、それは定義されていないので、method_missingメソッドが呼び出される。method_missingメソッドの中でundefined_method_execメソッドが呼び出され… というように循環し、スタックオーバーフローする。
Rubyの壊し方
# frozen_string_literal: true
require './tricks'
p '123456789'
上のスクリプトの実行結果は以下のようになる。
もちろん、仕掛けはある。この一行だ。
require './trick'
これは同じ階層にあるtrick.rbというファイルを読み込んでいる。
trick.rbの中身は以下のようになっている。
# frozen_string_literal: true
module Kernel
alias old_p p
def p(text)
old_p(text.reverse)
end
end
trick.rbでは組み込みのKernelモジュールを開けて、pメソッドを再定義している。
ただ、これではtrick.rbを読み込んだファイル全体でpメソッドが再定義されてしまう。
以下のようにすると使いたいところで使いたいメソッドをKernelモジュールに定義できる。
refineキーワードを用いて別の場所で定義した内容をusingキーワードがある場所のみに適用できる。
# refine_kernel.rbの中身
module KernelExtentions
refine Kernel do
def refine_p
p '再定義されました'
end
end
end
# refine_kernel.rbと同じ階層のrubyスクリプト
require './refine_kernel'
using KernelExtentions
refine_p
最後に
記載内容はメタプログラミングRuby 第2版を参考に、一部ソースコードを書き換えています。