🤸‍♂️

Rubyのメタプログラミングで動的にメソッドを操ってみる

2024/01/28に公開

どうもお疲れ様です。MESIです。
Rubyのメタプログラミングで動的なメソッドの呼び出しと定義方法について学習したので、忘備録として残します。

動的なメソッドの呼び出しと定義について

sendで動的にメソッドを呼び出す

sendを利用することで動的にメソッドを呼び出すことができます。

object.send(method_name, *arguments)
class MyClass
  def hello
    "Hello, world!"
  end
end

obj = MyClass.new
obj.hello # => "Hello, world!"
puts obj.send(:hello)  # => "Hello, world!"

このようにメソッドの呼び出しにsendを使って呼び出すことができます。

sendを使ってメソッドを呼びだすと何がうれしいのか?

通常のドット記法ではなく、なぜsendを使うのか?
それはsendを使ってメソッドを呼び出すと以下のメリットが得られるためです。

柔軟性

メソッド名や引数を動的に決定できるため、柔軟なコーディングが可能です。特にメタプログラミングやDSL(ドメイン特化言語)の実装において有用です。
コードの実行時に呼び出すメソッドを決められることを動的ディスパッチと言います。

サンプル

class Calculator
  def add(a, b)
    a + b
  end

  def subtract(a, b)
    a - b
  end

  def multiply(a, b)
    a * b
  end

  def divide(a, b)
    a / b
  end
end

calculator = Calculator.new

# ユーザーからの入力をシミュレート
method_name = :multiply  # 例: multiply
arguments = [10, 5]      # 例: 10と5を掛ける

# sendを使って動的にメソッドを呼び出す
result = calculator.send(method_name, *arguments)

puts "Result: #{result}"  # => "Result: 50"

プライベートメソッドの呼び出し

sendはプライベートメソッドも呼び出すことができます。これにより、通常の方法ではアクセスできないメソッドにもアクセス可能になります。

class SecretCalculator
  def initialize(x)
    @x = x
  end

  private

  def square
    @x * @x
  end

  def cube
    @x * @x * @x
  end
end

calculator = SecretCalculator.new(3)

# プライベートメソッド 'square' を呼び出す
square_result = calculator.send(:square)
puts "Square: #{square_result}"  # => "Square: 9"

# プライベートメソッド 'cube' を呼び出す
cube_result = calculator.send(:cube)
puts "Cube: #{cube_result}"      # => "Cube: 27"

define_methodでメソッドを動的に定義する

Module#define_methodを使用することで動的にメソッドを定義することができます。

define_method(:method_name) do |arg1, arg2, ...|
  # メソッドの本体
end

ここで、:method_nameは定義するメソッドの名前(シンボル)、do ... endのブロック内が新しいメソッドの本体です。
このブロックは、メソッドが呼ばれたときに実行されます。

class Greeter
  # 動的にhelloメソッドを定義
  define_method(:hello) do |name|
    "Hello, #{name}!"
  end
end

greeter = Greeter.new
puts greeter.hello("World")  # => "Hello, World!"

define_methodでメソッドを定義すると何がうれしいのか?

define_methodでメソッドを定義すると以下のメリットが得られます。

ダイナミックなメソッド定義ができる

プログラムの実行時にメソッドを生成することができます。
これは、プログラムの振る舞いを実行時に変更する必要がある場合に非常に有効です。
例えば、ユーザー入力や外部データに基づいてメソッドを定義することができます。

class DynamicMethods
  ['one', 'two', 'three'].each do |method_name|
    define_method("print_#{method_name}") do
      puts "Method name is #{method_name}"
    end
  end
end

d = DynamicMethods.new
d.print_one   # => "Method name is one"
d.print_two   # => "Method name is two"
d.print_three # => "Method name is three"

このような振る舞いを利用してAPIの動的な拡張を行えたりします。

class APIWrapper
  {
    get_user: '/users',
    get_products: '/products',
    get_orders: '/orders'
  }.each do |method_name, endpoint|
    define_method(method_name) do
      "Fetching data from #{endpoint}"
    end
  end
end

api = APIWrapper.new
puts api.get_user      # => "Fetching data from /users"
puts api.get_products  # => "Fetching data from /products"
puts api.get_orders    # => "Fetching data from /orders"

sendとdefine_methodを組み合わせてDRY(Don't Repeat Yourself)原則の強化をする

以下のコードでは、四則演算を行うメソッドを一つのループで定義しています。
これにより、似たようなメソッドを繰り返し書く必要がなくなります。

class Calculator
  ['add', 'subtract', 'multiply', 'divide'].each do |operation|
    define_method(operation) do |a, b|
      a.send(operation, b)
    end
  end
end

calc = Calculator.new
puts calc.add(10, 5)       # => 15
puts calc.subtract(10, 5)  # => 5
puts calc.multiply(10, 5)  # => 50
puts calc.divide(10, 5)    # => 2

以下のコードは文字列操作のためのメソッドを動的に定義した例です。
メソッド名と対応する文字列操作をマッピングすることで、コードの重複を減らします。

class StringTransformer
  {
    upcase: :upcase,
    downcase: :downcase,
    reverse: :reverse
  }.each do |method_name, string_method|
    define_method(method_name) do |str|
      str.send(string_method)
    end
  end
end

transformer = StringTransformer.new
puts transformer.upcase("hello")   # => "HELLO"
puts transformer.downcase("HELLO") # => "hello"
puts transformer.reverse("hello")  # => "olleh"

他の動的なメソッド定義と呼び出し方法について

method_missingを使うことでも動的なメソッド定義、呼び出しを実現することができます。
method_missingについてはこちらの記事で紹介しています。
https://zenn.dev/mesi/articles/9b4590e817cc56

Discussion