LoginSignup
5
0

More than 1 year has passed since last update.

メタプログラミングに触れてみた

Last updated at Posted at 2022-06-21

メタプログラミングとは

言語要素を実行時に操作するコードを記述すること

言語要素

  • 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というメソッドが呼び出されている。ただ、このスクリプトは問題なく実行できる。
実行結果は以下の通り。
image.png

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

実行結果は以下の通り。
image.png

ひとつ前のスクリプトと同様に、sampleオブジェクトには定義されていない、hogeというメソッドを呼び出している。ただ、エラーにはならない。
Sampleクラスの継承のルートにはRubyの組み込みクラスであるBasicObjectクラスが存在する。Rubyの場合、組み込みのクラスであっても、その定義を書き換えることができる。この方法をオープンクラスと呼ぶ。
通常、定義されていないメソッドの呼び出しを行うと、以下のようなエラーになる。

image.png

この際、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メソッドが呼び出され… というように循環し、スタックオーバーフローする。

image.png

Rubyの壊し方

# frozen_string_literal: true

require './tricks'

p '123456789'

上のスクリプトの実行結果は以下のようになる。

image.png

もちろん、仕掛けはある。この一行だ。

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版を参考に、一部ソースコードを書き換えています。

5
0
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
5
0