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

# メタプログラミングとは

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


# 言語要素

- classやmethod、moduleなど、エディタ上で定義したままのプログラムの構成要素
- 通常、プログラムの実行時には言語要素は実体を失っているため、定義を変更することはできない


<strong>ただし、Rubyを除いては</strong>

# コードを書くコードを書く

## インスタンスメソッドを動的に生成する

```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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1625409/eee970d0-f6bc-a1af-66dd-6f5c2ff8f40f.png)<br>

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のリファレンス](https://docs.ruby-lang.org/ja/latest/method/Module/i/define_method.html)
そのため、method_listの要素として渡された文字列はsampleオブジェクトのメソッドとして呼び出すことができるようになる。

## エラーを捕捉してメソッドを生成する
```ruby
# 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1625409/4d90366f-f508-0a03-6e3f-3efb5a6212d5.png)<br>

ひとつ前のスクリプトと同様に、sampleオブジェクトには定義されていない、hogeというメソッドを呼び出している。ただ、エラーにはならない。
Sampleクラスの継承のルートにはRubyの組み込みクラスであるBasicObjectクラスが存在する。Rubyの場合、組み込みのクラスであっても、その定義を書き換えることができる。この方法を<strong>オープンクラス</strong>と呼ぶ。
通常、定義されていないメソッドの呼び出しを行うと、以下のようなエラーになる。<br>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1625409/3cd9699f-f1ff-f6f1-6f02-9a6e6e5565bd.png)<br>
この際、BasicObjectのmethod_missingメソッドが呼び出されるのだが、これをオーバーライドして、受け取った名前のメソッドをその場で生成しているのだ。
ただ、この方法は非常に危険だ。

```ruby
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1625409/98db3b9c-4d11-0c6a-3462-f0c2b1368fbe.png)<br>

# Rubyの壊し方

```ruby
# frozen_string_literal: true

require './tricks'

p '123456789'
```

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1625409/88c2914d-8b3b-ef30-e59f-f5360df7de18.png)<br>

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

```ruby
require './trick'
```

これは同じ階層にあるtrick.rbというファイルを読み込んでいる。
trick.rbの中身は以下のようになっている。

```ruby
# 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キーワードがある場所のみに適用できる。
```ruby
# 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版](https://www.oreilly.co.jp/books/9784873117430/)を参考に、一部ソースコードを書き換えています。