ピュア Ruby で Ruby 2.7 の Numbered parameter を実装してみよう！


[Ruby Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ruby) 1日目の記事になります。
本記事では Ruby 2.7 で実装される Numbered parameter っぽい機能をピュアRuby で実装してみたいと思います。
またこの記事の実装は以下の記事を参考にして書いています。

* [Rubyのブロックつらい問題を解決する暗黙のブロックパラメータ - Qiita](https://qiita.com/supermomonga/items/b0576847b1b88e3cd400)

4年以上前にこういうのが書かれていたのすごい。


## Numbered parameter とは

Numbered parameter、略してナンパラです。
ナンパラは『暗黙的にブロックの引数を参照する構文』になります。
通常ブロックで引数を受け取る場合、仮引数を定義して受け取ります。

```ruby
# it という名前の仮引数を定義して、それで引数を参照する
[1, 2, 3].map { |it| it.to_s + it.to_s } # => ["11", "22", "33"]
```

ナンパラでは仮引数を定義するのではなくて `_1` という記号で『暗黙的に第一引数を参照する』事が出来るようになります。

```ruby
# ナンパラを使うと仮引数を定義する事なく引数が参照できる
# _1 は第一引数を参照する
[1, 2, 3].map { _1.to_s + _1.to_s } # => ["11", "22", "33"]
```

ナンパラは `_1 ~ _9` を使用することが出来ます。
これを利用することでより簡潔に Ruby のコードを記述する事が出来ます。

```ruby
# ナンパラを使わない難解な Ruby のコード
%w(homu mami mado).map(&:upcase)
%w(homu mami mado).each(&method(:puts))
%w(homu mami mado).map(&"name is ".method(:+))


# ナンパラを使った簡潔な Ruby のコード
%w(homu mami mado).map { _1.upcase }
%w(homu mami mado).each { puts _1 }
%w(homu mami mado).map { "name is " + _1 }
```

今回はこの `_1` をピュア Ruby でも実装してみたいと思います。

ちなみに `_1` という名前は Ruby 2.6 現在でも変数名やメソッド名などで使用することが可能です。

```ruby
# 変数名として定義できる
_1 = 42
_2 = _1 + _1
p _2   # => 84

# メソッド名として定義できる
def _3
  "three"
end

p _3   # => "three"
```

また、上記のコードは Ruby 2.7 でも引き続き動作します。
ただし、Ruby 2.7 から変数名に `_1` を使用した場合は警告が出るようになったので注意して下さい。

```ruby
# warning: `_1' is used as numbered parameter
_1 = 42
```

ちなみに本記事で書かれている Ruby のコードは Ruby 2.7 では意図する動作はしないので注意して下さい。


## 動作イメージ

まず簡単な動作イメージを考えます。

```ruby
class X
  def triple(n, &block)
    block.call(n, n, n)
  end
end

x = X.new
p x.triple(42) { _1.to_s + _2.to_s + _3.to_s }
# => "424242"
```

ナンパラと同様にブロック内で `_1` などを参照することを想定しています。
ポイントとしては、

* `_1` をどこで定義するのか
* ブロックの中身を評価するコンテキストはどこになるのか
* それをどうやって既存のメソッドに適用させるか

あたりでしょうか。
では、1つずつ実装していきましょう。


## `_1` を参照するためのクラスを定義する

最初にナンパラの要である `_1` を定義してみましょう。
これは次のようなクラスとして定義しておきます。

```ruby
module Nanpara
  class Args
    def initialize(*args)
      @args = args
    end

    def _1
      @args[0]
    end

    def _2
      @args[1]
    end

    def _3
      @args[2]
    end
  end
end

# Args.new にメソッドの引数を渡す想定
nanpara = Nanpara::Args.new("homu", "mami", "mado")

# new に渡した引数をそのまま返す
p nanpara._1    # => "homu"
p nanpara._2    # => "mami"
p nanpara._3    # => "mado"
```

これ自体はとてもシンプルですね。
次はこの `_1` というメソッドをナンパラのように呼び出してみます。


## `#instance_exec` を利用してブロックのコンテキストを切り替える

Ruby には `#instance_exec` というとても便利なメソッドが定義されています。
これは『レシーバを `self` としてブロックを実行する』というメソッドになります。
どういうことかよくわかりませんね。
実際の使用例を見てみましょう。

```ruby
# instance_exec に渡したブロックないは "hoge" のコンテキストとして実行される
"hoge".instance_exec {
  # ここの self は "hoge" になる
  p self     # => "hoge"
  # レシーバがない場合は "hoge" のメソッドを参照する
  p length   # => 4
}
```

こんな感じで『`"hoge"` のコンテキストでブロックを実行する事』が出来ます。
この `#instance_exec` と先程定義した `Nanpara::Args` クラスを組み合わせることでナンパラのようなブロックを実行する事が出来ます。

```ruby
module Nanpara
  class Args
    def initialize(*args)
      @args = args
    end

    def _1
      @args[0]
    end

    def _2
      @args[1]
    end

    def _3
      @args[2]
    end
  end
end

nanpara = Nanpara::Args.new("homu", "mami", "mado")

# nanpara のコンテキストでブロックを実行する
nanpara.instance_exec {
  # Nanpara のコンテキストとして実行される
  p self
  # => #<Nanpara:0x000055f8cd6ddb68 @args=["homu", "mami", "mado"]>

  # self を付けないで _1 を呼び出す事が出来る！！
  p _1 + _2 + _3
  # => "homumamimado"
}


# 特定の proc で _1 を使いたい場合、Nanpara::Args を経由して呼び出す必要がある
twice = proc { _1 + _1 }
args = ["homu"]

p Nanpara::Args.new(*args).instance_exec(&twice)
# => "homuhomu"
```

もうナンパラじゃん！！！
と、いう感じでブロック内でナンパラっぽい構文を書くことが出来るようになります。
やったー！！！
さて、ここからちょっとコードを整理してきます。


## `_1` を動的に定義する

現状は `_1 ~ _3` までを一つずつ定義しています。
Ruby では動的にメソッドを定義することが出来るので `_1 ~ _9` まで動的にメソッドを定義するようにします。

```ruby
module Nanpara
  class Args
    def initialize(*args)
      @args = args
    end

    # define_method を使って動的に _1 ~ _9 までメソッド定義する
    (1..10).each { |n|
      define_method(:"_#{n}") {
        @args[n-1]
      }
    }
  end
end
```

だいぶコンパクトになりましたね！


## `Proc` にヘルパメソッドを定義する

毎回 `Nanpara::Args.new(*args).instance_exec(&block)` みたいに呼び出すのはちょっとつらいですね。
`Proc` に新しいメソッドを定義してもう少し使いやすくしてみましょう。
当然 Refinements を使ってメソッドを定義します。
Refinements についてもっと知りたい方は去年書いた記事を読んで下さい！

* [【Ruby Advent Calendar 2018】あなたのしらない Refinements の世界【3日目】 - Secret Garden(Instrumental)](http://secret-garden.hatenablog.com/entry/2018/12/03/231925)

Refinements を使用して `Proc` を拡張すると以下のようになります。

```ruby
module Nanpara
  class Args
    def initialize(*args)
      @args = args
    end

    (1..10).each { |n|
      define_method(:"_#{n}") {
        @args[n-1]
      }
    }
  end

  # Proc にメソッドを追加する
  # 特定のコンテキストでのみ使いたいの Refinements で定義する
  refine Proc do
    # 自身のブロック内で _1 を参照できるような Proc にして返す
    def nanparable
      # ブロックに仮引数が定義されない場合のみナンパラが使えるようにする
      tap { break proc { |*args| ::nanpara::args.new(*args).instance_exec(&self) } if parameters.empty? }
    end
  end
end

# Proc#nanparable を使えるように宣言
using Nanpara

# proc 内で _1 が使えるような proc に変換して返す
nanpara = proc { _1 + _2 + _3 }.nanparable

# あとは普通に呼び出すだけ
p nanpara.call("homu", "mami", "mado")
# => "homumamimado"


# Proc#nanparable をブロック引数に渡すことで
# 既存のメソッドに対しても _1 を使用することが出来る
p (1..10).map(&proc { _1 + _1 }.nanparable)
# => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
```

これでだいぶ使い勝手がよくなりましたね！
ここから既存のメソッドで `_1` が使えるように拡張できるような仕組みを考えていきたいと思います。

## `prepend` を使ってメソッドをラップする

元々のやりたいことは以下のように既存のメソッドで `_1` を使いたいことでした。

```ruby
class X
  def triple(n, &block)
    block.call(n, n, n)
  end
end

x = X.new
p x.triple(42) { _1.to_s + _2.to_s + _3.to_s }
# => "424242"
```

`X#triple` というメソッドを拡張して `_1` を使ったブロックを評価できるようにする事を考えてみましょう。
ここで必要なのは、

* `X#triple` というメソッドを上書きする
* 上書きしたメソッドで `Proc#nanparable` を呼び出す

の 2点です。
と、いうわけでこの 2点を実装すると以下のようになります。

```ruby
module Nanpara
  # 省略
end

class X
  def triple(n, &block)
    block.call(n, n, n)
  end

  # prepend することで ↑ の triple よりも先に呼び出されるメソッドを定義できる
  prepend Module.new {
    # Proc#nanparable を使いたいので using しておく
    using Nanpara

    # この triple は X#triple よりも先に呼び出される
    def triple(n, &block)
      # super は X#triple を呼び出す
      # このタイミングで nanparable したブロックを渡すようにする
      super(n, &block.nanparable)
    end
  }
end

x = X.new

# prepend した module 無いのメソッドが先に呼び出される
# これにより既存の X#triple を変更する事なく _1 を使用することが出来る
p x.triple(42) { _1.to_s + _2.to_s + _3.to_s }
# => "424242"
```

Ruby では `prepend` を使用することで既存のメソッドを上書きすることなくメソッド呼び出しに処理をフックすることが出来ます。
こんな感じで既存のメソッドに対して `_1` を使えるようにしていきます。


## メソッドをナンパラ化するヘルパメソッドを定義する

先程のように `prepend` することで既存のメソッドをナンパラ化することができました。
では、既存のメソッドをナンパラ化するようなヘルパメソッドを定義してみましょう。
イメージとしては以下のような感じです。

```ruby
class X
  using Nanpara

  def triple(n, &block)
    block.call(n, n, n)
  end

  # use_numbered_paramters にメソッド名を渡すとそのメソッドがナンパラ化する
  use_numbered_paramters :triple
end
```

クラス内で使用できるメソッドを定義する場合は `Module` クラスのインスタンスメソッドを定義します。
今回も Refinements を使って `Module` クラスに `#use_numbered_paramters` というメソッドを追加します。

```ruby
module Nanpara
  class Args
    def initialize(*args)
      @args = args
    end

    (1..10).each { |n|
      define_method(:"_#{n}") {
        @args[n-1]
      }
    }
  end

  refine Proc do
    def nanparable
      tap { break proc { |*args| ::Nanpara::Args.new(*args).instance_exec(&self) } if parameters.empty? }
    end
  end

  # Module に対してインスタンスメソッドを定義する Refinements
  refine Module do
    using Nanpara

    # 引数のメソッドをナンパラ化する
    def use_numbered_paramters(*method_names)
      # 引数無い場合は全メソッドを対象とする
      method_names = instance_methods if method_names.empty?

      # メソッド内で prepend するぞ！
      prepend Module.new { |mod|
        method_names.each { |name|
          define_method(name) { |*args, &block|
            super(*args, &block&.nanparable)
          }
        }
      }
    end
  end
end

class X
  using Nanpara

  def triple(n, &block)
    block.call(n, n, n)
  end

  # use_numbered_paramters にメソッド名を渡すとそのメソッドがナンパラ化する
  use_numbered_paramters :triple
end

x = X.new

# X#triple で _1 を使用することが出来る
p x.triple(42) { _1.to_s + _2.to_s + _3.to_s }
# => "424242"
```

ちょっと複雑ですがこんな感じで実装する事が出来ます。
また、これを利用することで既存のクラスをシュッとナンパラ化することが出来ます！！！

```ruby
# Array のメソッドをナンパラ化する！
class Array
  using Nanpara
  use_numbered_paramters
end

p (1..10).to_a.shuffle.sort { _2 <=> _1 }
# => [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
p (1..10).to_a.inject(100) { _1 + _2 }
# => 155
```

もうナンパラじゃん。


## もっと簡単にクラスをナンパラ化できるようにする

ほぼ完成なんですが、最後にもっと簡単に安全にクラスをナンパラ化する仕組みを考えてみましょう。
先程の例であれば以下のようにして任意のクラスをナンパラ化することが出来ます。

```ruby
class Array
  using Nanpara
  use_numbered_paramters
end
```

しかし、この場合は `Array` を直接書き換えてしまっているので `Array` を参照している場所全てに影響を及ぼしてしまいます。
これはよくないですね。
そこで以下のような仕組みで『特定のコンテキストでのみナンパラ化』してみます。

```ruby
module Nanpara
  class Args
    def initialize(*args)
      @args = args
    end

    (1..10).each { |n|
      define_method(:"_#{n}") {
        @args[n-1]
      }
    }
  end

  refine Proc do
    def nanparable
      tap { break proc { |*args| ::Nanpara::Args.new(*args).instance_exec(&self) } if parameters.empty? }
    end
  end

  refine Module do
    using Nanpara
    def use_numbered_paramters(*method_names)
      method_names = instance_methods if method_names.empty?
      prepend Module.new { |mod|
        method_names.each { |name|
          define_method(name) { |*args, &block|
            super(*args, &block&.nanparable)
          }
        }
      }
    end
  end

  using Nanpara
  def self.forward_use_numbered_paramters(mod)
    mod.use_numbered_paramters
  end

  def self.const_missing(klass_name)
    klass = Object.const_get(klass_name)
    ::Module.new do
      refine klass do
        # 本来であれば use_numbered_paramters を直接呼び出したいが Refinements のバグで呼び出せない…
        # Ruby 2.7 だとこの挙動は修正されていた
        # use_numbered_paramters

        # 致し方なくメソッドを経由して呼び出す
        Nanpara.forward_use_numbered_paramters(self)
      end
      # クラスメソッドにも反映させる
      refine klass.singleton_class do
        Nanpara.forward_use_numbered_paramters(self)
      end
    end
  end
end

class X
  def triple(n, &block)
    block.call(n, n, n)
  end
end

# Nanpara::クラス名 で任意のクラスをナンパラ化する
using Nanpara::X

x = X.new

# X#triple で _1 を使用することが出来る
p x.triple(42) { _1.to_s + _2.to_s + _3.to_s }
# => "424242"
```

上の実装であれば `using Nanpara::クラス名` で任意のクラスをナンパラ化することが出来ます。
また、Refinements を利用してナンパラ化しているので次のように『任意のコンテキストでのみ』ナンパラ化することが出来ます。

```ruby
module ArrayEx
  # このコンテキストでのみ Array をナンパラ化する
  using Nanpara::Array

  p (1..10).to_a.shuffle.sort { _2 <=> _1 }
  # => [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
  p (1..10).to_a.inject(100) { _1 + _2 }
  # => 155
end

# 上のコンテキスト以外では Array はナンパラ化しないので最小限の副作用で抑えられる
# Error: undefined local variable or method `_2' for main:Object (NameError)
p (1..10).to_a.shuffle.sort { _2 <=> _1 }
```

もう最強じゃん…。

## 最終的に出来上がったもの

最終的に出来上がったものがこちらになります。

```ruby
module Nanpara
  class Args
    def initialize(*args)
      @args = args
    end

    (1..10).each { |n|
      define_method(:"_#{n}") {
        @args[n-1]
      }
    }
  end

  refine Proc do
    def nanparable
      tap { break proc { |*args| ::Nanpara::Args.new(*args).instance_exec(&self) } if parameters.empty? }
    end
  end

  refine Module do
    using Nanpara
    def use_numbered_paramters(*method_names)
      method_names = instance_methods if method_names.empty?
      prepend Module.new { |mod|
        method_names.each { |name|
          define_method(name) { |*args, &block|
            super(*args, &block&.nanparable)
          }
        }
      }
    end
  end

  using Nanpara
  def self.forward_use_numbered_paramters(mod)
    mod.use_numbered_paramters
  end

  def self.const_missing(klass_name)
    klass = Object.const_get(klass_name)
    ::Module.new do
      refine klass do
        Nanpara.forward_use_numbered_paramters(self)
      end
      refine klass.singleton_class do
        Nanpara.forward_use_numbered_paramters(self)
      end
    end
  end
end
```

ちょうど 50行でナンパラっぽいものを実装することが出来ました。
実際は Refinements で拡張するコードも含まれているのでナンパラ自体の実装は30行ぐらいしかありません。
これだけのコードでナンパラを実装できる Ruby すごくないですか？
こういう<del>変態的な</del>コードを書けるのが Ruby の楽しいところですよねー。
Ruby たのしー。
みなさんもどんどん Ruby のよくわからないコードを書いていきましょう！


## Ruby 2.7 のナンパラとの違い

最後に Ruby 2.7 のナンパラとの違いを説明しておきます。

### `Proc#parameters` の戻り値

Ruby 2.7 のナンパラはパース時に `_1` の検知を行っています。
なので使用している `_1` を考慮した情報を返します。

```ruby
p Proc.new { _1 + _2 }.parameters
# => [[:opt, :_1], [:opt, :_2]]
```

しかし、今回作成したものは動的に処理しているため `Proc#parameters` で取得することは出来ません。

```ruby
using Nanpara::Proc

p Proc.new { _1 + _2 }.parameters
# => [[:rest, :args]]
```

### 配列を渡した時の違い

Ruby 2.7 のナンパラは `_1` を使用したら `|_1`、 `_1` と `_2` を使用したら `|_1, _2|` という風に受け取ります。
なので配列を渡した場合に『`_2` 以上を使用してれば配列を展開して』受け取りますた。

```ruby
p Proc.new { [_1] }.call [1, 2]      # => [[1, 2]]
p Proc.new { [_1, _2] }.call [1, 2]  # => [1, 2]
```

しかし、今回作成したものは可変長引数として受け取っているためです。

```ruby
using Nanpara::Proc
p Proc.new { [_1] }.call [1, 2]      # => [[1, 2]]
p Proc.new { [_1, _2] }.call [1, 2]  # => [[1, 2], nil]
```



## まとめ

と、いう感じで ピュア Ruby で Ruby 2.7 の Numbered parameter っぽい機能を実装してみました。
完璧に同じもの！とは言えませんが 50行ぐらいの実装でそれっぽいものをつくることが出来ました。
Ruby だとコードレベルでこういうことを実現することができるのがとても面白いですね。
Ruby たのしー。

ちなみにこの実装を書いたのは 2回目になり、1回目は gem として公開してあります。

* [Ruby で暗黙のブロックパラメータを使うための gem をつくった - Secret Garden(Instrumental)](http://secret-garden.hatenablog.com/entry/2016/03/12/235700)

こっちは 3年以上前に書いたものになります。
基本的な使い方は今回書いたコードとだいたい同じなんですが実装は結構違っているので気になる方は実装を読んでみると面白いかもしれません。
ちなみにこの gem だと `_1` 以外にも `_yield` や `_self`、`_receiver` などといった値にも参照することが出来ます。
そのため『自身を呼び出した再帰処理』を記述する事が出来ます。

```ruby
# 再帰
fact = proc { _1 == 1 ? 1 : _1 * _self.(_1 - 1); }.use_args
p fact.call 5
# => 120

p [1, 2, 3].use_args.map { _1 + _receiver.size }
# => [4, 5, 6]
```

これは Ruby 2.7 の Numbered parameter にはない強みですね。
[supermomonga さんが書いた元ネタの記事](https://qiita.com/supermomonga/items/b0576847b1b88e3cd400)は 4年以上前に書かれており、その時は Numbered parameter の話は全く出ていなかったんですが、それが Ruby 2.7 で実装されるのは感慨深いものがありますね。
Ruby 2.7 のナンパラは簡単そうにみえて実は結構癖が強いので実際使われてみてどうなるのかは気になりますね。
もう 12月で Ruby 2.7 のリリースまでもうすぐですが果たして無事にナンパラをリリースする事が出来るのか…。
と、言う感じで今年の Ruby Advent Calendar を書いてみました。