Rubyのsendメソッドとメタプログラミングを少しだけ理解した

## はじめに
Rubyでは通常のメソッド呼び出しとは別に、`send`メソッドでメソッドを呼び出すことができます。

この記事では`send`メソッドの使い方とメタプログラミングとの関連、使用する際の注意点についてまとめます。

なおこの記事におけるRubyのバージョンは**3.3**です。

## `send`メソッドとは？
`send`メソッドはレシーバが持つメソッドを文字列またはシンボルで指定して呼び出せるメソッドです。

実際のコードで確認します。

```ruby
class Greeting
  def hello
    puts 'Hello, World!'
  end
end

greeting = Greeting.new

# 通常のメソッド呼び出し
greeting.hello
#=> Hello, World!

# sendメソッドを使ったメソッド呼び出し(文字列)
greeting.send('hello')
#=> Hello, World!

# sendメソッドを使ったメソッド呼び出し（シンボル）
greeting.send(:hello)
#=> Hello, World!
```

第二引数以降には呼び出すメソッドへ渡す引数を指定できます。

```ruby
class Greeting
  def hello(name, i)
    i.times do
      puts "Hello, #{name}!"
    end
  end
end

greeting = Greeting.new

# 通常のメソッド呼び出し
greeting.hello('Taro', 3)
# Hello, Taro!
# Hello, Taro!
# Hello, Taro!

# sendメソッドを使ったメソッド呼び出し(文字列)
greeting.send('hello', 'Taro', 3)
# Hello, Taro!
# Hello, Taro!
# Hello, Taro!

# sendメソッドを使ったメソッド呼び出し（シンボル）
greeting.send(:hello, 'Taro', 3)
# Hello, Taro!
# Hello, Taro!
# Hello, Taro!

```

## 動的なメソッド呼び出し
`send`メソッドの魅力は動的なメソッド呼び出しが可能な点です。

レシーバの持つメソッドを動的に呼び出すことでコードの重複を避けることができます。

たとえば以下のコードがあるとします。

```ruby
class Animal
  def sound(type)
    case type
    when 'dog'
      dog_sound
    when 'cat'
      cat_sound
    when 'cow'
      cow_sound
    end
  end

  def dog_sound
    puts 'ワン'
  end

  def cat_sound
    puts 'ニャー'
  end

  def cow_sound
    puts 'モー'
  end
end

animal = Animal.new
animal.sound('cat')
#=> ニャー
```

引数`animal_name`の中身に応じて`case`文で３パターンの条件分岐を記述しています。

これを`send`メソッドを使って書き換えてみます。

```ruby
class Animal
  attr_accessor :type

  def initialize(type)
    @type = type
  end

  def dog_sound
    puts 'ワン'
  end

  def cat_sound
    puts 'ニャー'
  end

  def cow_sound
    puts 'モー'
  end
end

animal = Animal.new('cat')
animal.send("#{animal.type}_sound")
#=> ニャー
```

`send`メソッドを駆使することで`case`文による条件分岐を丸々書かずに実装できます。

そのほかに、たとえば次のような実装ができます。

### ①ファイル形式に応じたレポート出力
PDFやCSVなどファイル形式に応じた挙動が簡単に実現できます。

```ruby
class ReportGenerator
  def generate(format)
    send("generate_#{format}_report")
  end

  private

  def generate_pdf_report
    # 実際のPDFレポート生成ロジックを書く
    puts "PDF report generated"
  end

  def generate_html_report
    # 実際のHTMLレポート生成ロジックを書く
    puts "HTML report generated"
  end

  def generate_csv_report
    # 実際のcsvレポート生成ロジックを書く
    puts "Csv report generated"
  end
end

report_generator = ReportGenerator.new
report_generator.generate(:html)
#=> HTML report generated
```

### ②支払い方法に応じた決済処理
クレカ・銀行振込など支払い方法に応じた条件分岐も同様に実現できます。

```ruby
class PaymentProcessor
  def process(payment_method)
    send("#{payment_method}_payment")
  end

  private

  def credit_card_payment
    # 実際のクレジットカード決済処理を書く
    puts "Processing credit card payment"
  end

  def paypal_payment
    # 実際のPayPal決済処理を書く
    puts "Processing PayPal payment"
  end

  def bank_transfer_payment
    # 実際の銀行振込決済処理を書く
    puts "Processing bank transfer payment"
  end
end

payment_processor = PaymentProcessor.new
payment_processor.process(:bank_transfer)
#=> Processing bank transfer payment
```

## `send`メソッドとメタプログラミング
`send`メソッドは、Rubyにおけるメタプログラミングの重要なツールです。

メタプログラミングとは、「コードを書くコードを書くこと」を指し、プログラムの動作を動的に変更したり拡張したりする技術です。

`send`メソッドを使用することで、プログラムの実行時に動的にメソッドを呼び出すことができ、これによって柔軟なコードを書くことが可能になります。

OSSのコードで使われ方を見てみます。

### `Devise`の例
`Devise`は認証機能を提供するgemです。

モデルファイルで次のように使います。

```ruby:models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable
end
```

`devise`メソッドでUserモデルに様々な認証機能の追加が可能です。

この`devise`メソッドでは内部的に`send`メソッドが利用されています。

というわけで`Devise`の実装を見てみましょう。

```ruby:lib/devise/models.rb
# frozen_string_literal: true

module Devise
  module Models
    # ...省略...

    # Include the chosen devise modules in your model:
    #
    #   devise :database_authenticatable, :confirmable, :recoverable
    #
    # You can also give any of the devise configuration values in form of a hash,
    # with specific values for this model. Please check your Devise initializer
    # for a complete description on those values.
    #
    def devise(*modules)
      options = modules.extract_options!.dup

      selected_modules = modules.map(&:to_sym).uniq.sort_by do |s|
        Devise::ALL.index(s) || -1  # follow Devise::ALL order
      end

      devise_modules_hook! do
        include Devise::Orm
        include Devise::Models::Authenticatable

        selected_modules.each do |m|
          mod = Devise::Models.const_get(m.to_s.classify)

          if mod.const_defined?("ClassMethods")
            class_mod = mod.const_get("ClassMethods")
            extend class_mod

            if class_mod.respond_to?(:available_configs)
              available_configs = class_mod.available_configs
              available_configs.each do |config|
                next unless options.key?(config)
                send(:"#{config}=", options.delete(config))
              end
            end
          end

          include mod
        end

        self.devise_modules |= selected_modules
        options.each { |key, value| send(:"#{key}=", value) }
      end
    end

    # ...省略...
  end
end

require 'devise/models/authenticatable'
```

特に注目したいのはこの行です。

```ruby
send(:"#{config}=", options.delete(config))
```

たとえば`Devise`を次のように設定したとします。

```ruby
class User < ApplicationRecord
  devise :database_authenticatable, :registerable, :timeoutable, timeout_in: 30.minutes
end
```

この場合、先ほどの`send`メソッドで以下のような処理が行われます。

1. `:"#{config}="`は`:"timeout_in="`
2. `options.delete(config)`は`30.minutes`


つまり実質的に以下のコードと同じになります。

```ruby
self.timeout_in = 30.minutes
```

これによりタイムアウト時間が設定されるのです。

また以下の箇所でも`send`メソッドが使われています。

```ruby
options.each { |key, value| send(:"#{key}=", value) }
```

これは特定のモジュールに属さない設定オプションを処理します。

たとえば次の実装があったとします。

```ruby
class User < ApplicationRecord
  devise :database_authenticatable, :registerable, skip_session_storage: [:http_auth]
end
```

この場合、`skip_session_storage`は特定のモジュールに属さない一般的なオプションです。

`send(:"#{key}=", value)`の実行内容は以下と同様になります。

```ruby
self.skip_session_storage = [:http_auth]
```

この場面では`send`メソッドを使うことで以下の利点が得られます。

- どのような設定オプションが来ても対応するセッターメソッドを動的に呼び出せる
- 多数のオプションに対して個別の条件分岐を書く必要がない
- 新しい設定オプションが追加された場合もこのコードはそのまま機能する

gemはさまざまなユースケースへの対応が求められます。

またそのための拡張性も欠かせません。

このように高い汎用性や拡張性が要求される場面で`send`メソッドのようなメタプログラミング技術が求められます。

## sendメソッドを使う際の注意点
`send`メソッドは強力な機能ではありますが、注意点もたくさんあります。

以下に主な注意点をまとめます。

### セキュリティリスク
`send`メソッドはプライベートメソッドを呼び出すことができてしまいます。

```ruby
class Sample
  private
  def secret_method
    puts "This is a private method!"
  end
end

keeper = Sample.new
keeper.send(:secret_method)  # => This is a private method!
```

このように本来アクセスできないはずのメソッドにアクセスできるのです。

なおRubyには`send`メソッドとは別に`public_send`メソッドも存在します。

こちらは`send`メソッドとの違いはプライベートメソッドを呼び出せないことです。

```ruby
class Sample
  private
  def secret_method
    puts "This is a private method!"
  end
end

keeper = Sample.new
keeper.public_send(:secret_method)
# => private method `secret_method' called for an instance of Sample (NoMethodError)
```

安全性を重視したい場合は`public_send`メソッドを使いましょう。

### 可読性の低下
`send`メソッドのもたらす高い柔軟性はOSSなど多様なユースケースをカバーするだけの汎用性が必要な場面では有効です。

しかし実務では高い汎用性というよりは特定のビジネスニーズ・ロジックに対応することが求められます。

そういった状況下で`send`メソッドを筆頭とするメタプログラミングを過度に活用してしまうと、逆に複雑さを増してしまいかねません。

gemの開発やOSSのコードリーディングのため`send`メソッドやメタプログラミングの知識を持つことは重要ですが、日々の業務では使用を控えめにした方が良さそうです。

## ※`send`メソッドの代替
Ruby 2.7以降では、`send`メソッドの代わりに`__send__`メソッドを使用することが推奨される場合があります。

`send`というメソッド名が他の目的で既に使用されている場合の名前衝突を避けるためです。

```ruby
class Sample
  def send(message)
    puts "Sending #{message}"
  end

  def greet
    puts "Hello!"
  end
end

sample = Sample.new
sample.send("a message")  # => Sending a message
sample.__send__(:greet)  # => Hello!
```

このように、`send`メソッドが別の目的で使用されている場合には、`__send__`メソッドを予備として使えます。
## おわりに
記事を書いたものの、メタプログラミングについてはまだ分かったような分かっていないような感覚が強いです。

OSSのコードリーディングなどを通じてさらに理解を深めていければと思います。

またこの記事に関して誤りがあるようでしたら、コメントいただけますと幸いです。

## 参考資料
https://docs.ruby-lang.org/ja/latest/method/Object/i/send.html

https://github.com/heartcombo/devise

https://www.amazon.co.jp/%E3%83%A1%E3%82%BF%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0Ruby-%E7%AC%AC2%E7%89%88-Paolo-Perrotta/dp/4873117437

https://cyberwave.jp/staff-blog/engineer/ruby-send-method/