Refinementsをブロックの中だけで有効にする

Refinementsをブロックの中だけで有効にするgemを実装しました。
https://rubygems.org/gems/with_refinements

リポジトリはこちら
https://github.com/hanachin/with_refinements

使い方と実装方法を紹介します。

## 正直な結論: Refinementsをブロックの中だけで有効にするのはむり

Ruby本体をいじらない限りできません。

## 無理やりの結論: Refinementsをusingした環境でブロックの内容をevalすればいい

1. Refinementsをusingした環境を用意する
2. ブロックのselfを1で用意した環境に置く
3. ブロックから見えるローカル変数を1で用意した環境に置く
4. ブロックからブロックの内容の文字列を取得する
5. 2で用意したselfのinstance_evalでブロックの内容の文字列をevalする
6. 5で変更されたローカル変数の内容をブロックのbindingに書き戻す

これでだいたいのコードはブロックスコープ風な感じで動きそうです。
今回6は実装していません。

## 使い方(短い例)

gemを入れます

```console
$ gem i with_refinements
```

こんなコードを用意して実行します

```sample.rb
require 'with_refinements'

using WithRefinements

with_refinements(Module.new { refine(Object) { def hi; puts __callee__; end } }) do
  hi
  # hi
end

hi
```

ブロックの中ではRefinementsが有効になっていて、ブロックの外ではRefinementsが有効になっておらずエラーが出るのが分かると思います。

```console
$ ruby sample.rb
hi
Traceback (most recent call last):
sample.rb:10:in `<main>': undefined local variable or method `hi' for main:Object (NameError)
```

## 実装

短いので全部のせます。

```rb
module WithRefinements
  class << self
    def clean_binding
      eval('module Class.new::CleanRoom; binding; end')
    end

    def code_from_block(block)
      iseq = RubyVM::InstructionSequence.of(block).to_a
      loc = iseq[4].yield_self {|h| h[:code_range] || h[:code_location] }
      path = iseq[7]
      File.readlines(path)[loc[0]-1..loc[2]-1].tap {|ls|
        ls[0], ls[-1] = ls[0][loc[1]..-1], ls[-1][0..loc[3]]
      }.join
    end
  end

  refine(Object) do
    def with_refinements(*ms, &block)
      # enable refinements
      b = WithRefinements.clean_binding
      b.local_variable_set(:__modules__, ms)
      b.eval('__modules__.each {|m| using m }')

      # setup block eval context
      bb = block.binding
      b.local_variable_set(:__self__, bb.eval('self'))
      bb.local_variables.each {|n| b.local_variable_set(n, bb.local_variable_get(n)) }

      # eval block code
      b.eval("__self__.instance_eval #{WithRefinements.code_from_block(block)}")
    end
  end
end
```

### clean_binding

Refinementsをusingするためのきれいなbindingを得るため、毎回使い捨てのモジュールを作っています。

```rb
def clean_binding
  eval('module Class.new::CleanRoom; binding; end')
end
```

### code_from_block

Ruby 2.5からブロックがソースコード中で定義された位置がInstructionSequenceに保持されるようになりました。`RubyVM::InstructionSequence.of`にブロックを渡すとそのブロックのInstructionSequenceが取れます。`RubyVM::InstructionSequence#to_a`を呼ぶとInstructionSequenceの内容が配列として返ってくるので、そこからソースコードの絶対パスとコード中の位置が取得できます。

ファイルのパスとブロック定義の文字列の位置が正確にわかるので、ファイルを読めば文字列としてブロックのコードを抜き出すことが出来ます :v: 

```rb
def code_from_block(block)
  iseq = RubyVM::InstructionSequence.of(block).to_a
  loc = iseq[4].yield_self {|h| h[:code_range] || h[:code_location] }
  path = iseq[7]
  File.readlines(path)[loc[0]-1..loc[2]-1].tap {|ls|
    ls[0], ls[-1] = ls[0][loc[1]..-1], ls[-1][0..loc[3]]
  }.join
end
```

Ruby 2.5ではcode_rangeというキーで取れますがRuby 2.6からはcode_locationというキーに変わったようです :muscle: (対応しました

### with_refinements

この中で冒頭に書いた1〜5までを実行しています。特にめぼしいことはしていませんが、usingするモジュールとブロックのselfを渡すためにローカル変数を2つ(`__modules__`, `__self__`)汚染しています。特に衝突よけを用意していないので、同じ名前のローカル変数があると不具合が発生する可能性があります。

```rb
def with_refinements(*ms, &block)
  # enable refinements
  b = WithRefinements.clean_binding
  b.local_variable_set(:__modules__, ms)
  b.eval('__modules__.each {|m| using m }')

  # setup block eval context
  bb = block.binding
  b.local_variable_set(:__self__, bb.eval('self'))
  bb.local_variables.each {|n| b.local_variable_set(n, bb.local_variable_get(n)) }

  # eval block code
  b.eval("__self__.instance_eval #{WithRefinements.code_from_block(block)}")
end
```

## 弱点

以下のように1行のブロックだとなぜかファイルの場所や位置がInstructionSequenceから取得できないため、実行するとエラーがでます。

```rb
with_refinements { bar }
```

## 使い方(長い例)

長い例を書くとこういう感じです

```rb
require 'with_refinements'

module Helloable
  refine(Object) do
    def hello
      puts :hello
    end
  end
end

module Hiable
  refine(Object) do
    def hi
      puts :hi
    end
  end
end


class Person
  using WithRefinements

  def initialize(with:)
    @things = Array(with)
  end

  def greet(&block)
    with_refinements(*@things, &block)
  end
end

a = Person.new(with: Hiable)
b = Person.new(with: Helloable)

a.greet do
  hello rescue hi
end

b.greet do
  hi rescue hello
end
```

## まとめ

Refinements最高!