本記事ではメタプログラミングについてふれます。
メタプログラミングは、Rubyの文法の中でも、
かなり応用の部類に入り、Procなどと同様にかなり使いこなすのが難しい文法です。
※ちなみにProcについてはこちらの記事で解説しています。
そもそもメタプログラミングはかなり応用の文法なので、
Rubyの基礎の文法の解説は、
下記の動画などをご参考ください。
メタプログラミングの実際の使用シーンは限定的で、
RailsなどのOSSの内部実装で使われているという印象が強い技術です。
ただ、Rubyを使う上で、
メタプログラミングの知識はやはり欠かせないと思うので、
本記事では、大まかな概要を解説します。
メタプログラミングとは何か?
Rubyは、その柔軟性と表現力豊かな言語機能により、メタプログラミングに非常に適しています。
メタプログラミングは、プログラムが自身の構造や振る舞いを解析・変更する能力を指し、
これにより、より柔軟で効率的なコードを書くことができます。
クラスやメソッドの定義を動的に変更したり、実行時にコードを生成したりすることができ、
端的にいうと、コードを記述するコードという感じになります。
Railsのソースコードとかを見ると、
このようなコードがたくさん使われています。
https://github.com/rails/rails/blob/main/activerecord/lib/active_record/enum.rb#L318
例えば、上記のRailsのコードの場合、
define_methodやpublic_sendなどが、
メタプログラミングの代表的なメソッドに該当します。
基本的にメタプログラミングを使うことによる利点は、
DRY原則(Don't Repeat Yourself)を実現し、コードの重複を減らすことができたり、
柔軟性を高め、動的な振る舞いを実現できることなどが挙げられます。
要するに、効率的なコードを書くための手段という感じですね。
有名なメソッドとしては、すでに出てきた、
define_method、public_send(send)や、
instance_eval、method_missing、evalなどが挙げれれます。
define_methodを使ってコードをDRYにする例
試しに、define_methodを使って、
コードをDRYにしてみたいと思います。
まず、下記のようなコードがあるとします。
class Calculator
def add(a, b)
puts "Performing add operation:"
puts "#{a} + #{b} = #{a + b}"
end
def subtract(a, b)
puts "Performing subtract operation:"
puts "#{a} - #{b} = #{a - b}"
end
def multiply(a, b)
puts "Performing multiply operation:"
puts "#{a} * #{b} = #{a * b}"
end
def divide(a, b)
puts "Performing divide operation:"
puts "#{a} / #{b} = #{a / b}"
end
end
calculator = Calculator.new
calculator.add(5, 3)
calculator.subtract(10, 4)
calculator.multiply(4, 2)
calculator.divide(10, 2)
Calculatroクラスに、
それぞれ足し算、引き算、掛け算、わり算のメソッドが定義されています。
ただ、puts performing
の部分がほとんど似通っています。
仮に、このクラスに更に商のあまりを出す計算だったり、累乗とかが追加されれば、
さらに冗長になるおそれがあります。
これをメタプログラミング(define_method)を使って書き直すと、
class Calculator
[:add, :subtract, :multiply, :divide].each do |operation|
define_method(operation) do |a, b|
puts "Performing #{operation} operation:"
case operation
when :add
puts "#{a} + #{b} = #{a + b}"
when :subtract
puts "#{a} - #{b} = #{a - b}"
when :multiply
puts "#{a} * #{b} = #{a * b}"
when :divide
puts "#{a} / #{b} = #{a / b}"
end
end
end
end
calculator = Calculator.new
calculator.add(5, 3)
calculator.subtract(10, 4)
calculator.multiply(4, 2)
calculator.divide(10, 2)
このようにputs performing
の部分は共通化することができました。
今回のケースのように単純な例だと、
メタプログラミングを使わないで実装したほうが直感的でわかりやすいかも知れませんが、
仮にputs performing
のような各メソッドで共通の処理がさらに追加される場合は、
差が顕著になってきます。
Railsなどのフレームワークは、大規模で複雑なので、
メタプログラミングを使って可能な限り処理をDRYにしているというイメージですかね。
sendとevalを使ったメタプログラミング例
次に、sendとevalを使った例を紹介します。
sendは、先程出てきたpublic_sendと似たようなメソッドで、
send(name, *args)みたいな感じで使用し、
nameで指定したmethodをargsを引数にして実行するという感じです。
https://docs.ruby-lang.org/ja/latest/method/Object/i/send.html
public_sendにすれば、publicメソッドだけ呼び出すことができるという感じです。
evalは、eval(expr)みたいに使用すると、
exprをRubyのプログラムとして実行するというメソッドです。
https://docs.ruby-lang.org/ja/latest/method/Kernel/m/eval.html
ただ、紹介しておいて言うのもあれですが、
基本的にevalの使用については慎重になるべきです。
というのも、evalは何でもできてしまうので、
例えば外部の入力をそのままevalに渡すようなコードを書いてしまうと、
悪意あるユーザーの入力で一発でセキュリティインシデントになります。
ちなみに、Rubocopの警告にもevalについては、
使用しないようにというものがあります。
https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Security/Eval
あくまで本記事では、
メタプログラミングの活用例としてevalを使うので、
もし実際にevalを使う場合は使用すべきか十分に検討していただけると。
さて、上記を踏まえて、
下記がevalとsendを使ったメタプログラミングのサンプルです。
class DynamicCalculator
OPERATIONS = %i[add subtract multiply divide]
def calculate(operation, a, b)
if OPERATIONS.include?(operation.to_sym)
send(operation, a, b)
else
puts "Invalid operation: #{operation}"
end
end
OPERATIONS.each do |op|
eval <<-RUBY
def #{op}(a, b)
puts "Performing #{op} operation:"
result = case :#{op}
when :add
a + b
when :subtract
a - b
when :multiply
a * b
when :divide
a / b
end
puts "\#{a} #{op} \#{b} = \#{result}"
end
RUBY
end
end
calculator = DynamicCalculator.new
calculator.calculate(:add, 5, 3)
calculator.calculate(:subtract, 10, 4)
calculator.calculate(:multiply, 4, 2)
calculator.calculate(:divide, 10, 2)
calculator.calculate(:power, 2, 3)
まず、OPERATIONS 定数で、実行可能な操作のリストを定義し、
:add、:subtract、:multiply、:divide の4つの操作が含まれています。
calculate メソッドは、send メソッドを使用して、与えられた操作名に対応するメソッドを動的に呼び出しています。
OPERATIONS.each do |op|
の部分では、eval を使用して動的にメソッドを定義しています。
各操作に対して、その操作名をメソッド名として持ち、引数 a と b を受け取り、その操作を実行します。
インスタンスを作成し、calculate メソッドを呼び出すことで、動的に生成されたメソッドを使用して計算を行います。
与えられた操作が OPERATIONS 定数に含まれていない場合は、"Invalid operation" というメッセージが表示します。
例えば、新しい操作を追加したい場合は、OPERATIONS 定数にその操作を追加するだけで、
その操作を使用できるようになるので、DRYという観点では有効な実装かと思われます、
class_evalとmethod_missingを使ったメタプログラミング
お次は、class_evalとmethod_missingを使った例です。
Rubyでは、定義されていないメソッドを呼び出すと、
おなじみの、undefined method `method_name' for # (NoMethodError)みたいなエラーがでます。
Rubyにおける method_missing は、オブジェクトが存在しないメソッドを呼び出したときに、
呼び出される、BasicObjectのメソッドで、上記のおなじみのエラーはこのmethod_missingメソッドで
定義されているデフォルトの例外にあたります。
https://docs.ruby-lang.org/ja/latest/method/BasicObject/i/method_missing.html
そこで、このmethod_missingをオーバーライドすれば、
存在しないメソッドを呼び出したときの処理を定義することができるため、
動的メソッド定義と似たようなことができそうです。
試しに、
class Sample
def method_missing(method, *args)
puts 'hello world'
end
end
sample = Sample.new
sample.test
みたいなふうにSampleクラスで、
親クラスのmethod_missingをオーバーライドし、
定義されていないtestメソッドを呼び出します。
そうすると、hello worldが出力されます。
これを使えば、確かにメタプログラミングできそうですね。
method_missing を定義することで、オブジェクトが未定義のメソッドを呼び出したときに、
そのメソッド呼び出しをキャプチャし、オブジェクトが適切に応答するようにカスタマイズできます。
Rubyのオブジェクト指向の柔軟性と動的な性質を最大限に活用する手段として使用できるかと思います。
これを踏まえて、method_missingとclass_evalを使用してメタプログラミング例を紹介します。
あるゲームがあって、そのコマンドをmethod_missingで動的に定義している感じです。
(※あくまでこんな感じになるよみたいなイメージの紹介なので中身のコードが少々冗長なのはお許しを)
class Game
def initialize
@player_position = [0, 0]
end
def method_missing(method_name, *args)
command_name = method_name.to_s
if command_name.start_with?('move_')
direction = command_name.split('_').last
define_direction_method(direction)
send(method_name, *args)
else
super
end
end
private
def define_direction_method(direction)
self.class.class_eval do
define_method("move_#{direction}") do |steps|
move_player(direction, steps)
end
end
end
def move_player(direction, steps = 1)
case direction
when 'north'
@player_position[1] += steps
when 'south'
@player_position[1] -= steps
when 'east'
@player_position[0] += steps
when 'west'
@player_position[0] -= steps
else
@player_position[1] *= steps
end
puts "Player moved to #{@player_position}"
end
end
game = Game.new
game.move_north(2)
game.move_east(1)
game.move_south(3)
game.move_west(3)
game.move_straight(5)# 新しい方向を追加した場合
class_evalは、Gameクラスに対して、
動的にメソッドを定義するために使用しています。
さて、例えば、Gameのインスタンスである、
game変数に、move_northというメソッドを呼び出すも、
そんなメソッドはないので、method_missingが呼ばれます。
メソッド名がmoveから始まるので、
ifの中のdefine_direction_methodが呼ばれます。
splitしているので、引数はnorthですね。
それでclass_evalの中でdefine_methodで
動的にmove_notrhというメソッドを定義します。
その中身はmove_playerのcase節のnorthになるという感じですね。
https://docs.ruby-lang.org/ja/latest/method/Module/i/class_eval.html
メタプログラミングの注意点
さて、このようにメタプログラミングについて解説してきましたが、
すでにお察しの通り、メタプログラミングは直感的とは言い難い記述です。
そのため、過度な使用はコードの可読性を低下させる可能性があります。
また、動的な変更やmethod_missingとかを乱用すると、予期せぬ副作用なども生じる可能性があるので、
Rubyに対する深い理解がないとなかなか使いこなすのが難しいのが正直なところです。
言うなれば、より抽象的な記法ともいえるので、
仮にメタプログラミングを使うのであれば、
行き当たりばったりではなく、きちんと設計の手順を踏んだほうが良いでしょう。
また、Rubyにはブロックなどの強力な機能があるため、
メタプログラミングを使わなくても十分なケースもあるかもしれません。
ただ、メタプログラミングはそれだけ扱いが難しいだけあって、
適切な設計のもと、実装することができれば、
非常に美しいコードになります。
実際、Ruby on Railsなどのコードはメタプログラミングをかなり使用しており、
その分、確かにソースコードを読む難易度は上がっていますが、
Railsの黒魔術みたいな便利さはメタプログラミングが支えていると言っても過言ではないので、
パワーは申し分ありません。
それにメタプログラミングを使用しなかったとしても、
メタプログラミングを知っていると知らないのでは、
雲泥の差があります。
既存のコードをメタプログラミングで置き換えられないかなどと
試行錯誤するのも、かなり勉強になると思います。
もし、実務でメタプログラミングを本格的に使用するのであれば、
下記の書籍を一読することをおすすめします。
かなり骨太な本ですが、
メタプログラミングを使いこなすのであれば必読の一冊でしょう。
ちなみに、この本は下記の記事でも紹介している一冊でもあります。
RubyやRailsはもちろんそれ以外の書籍についても、
おすすめを紹介しているので宜しければご参考ください。
-
参考【技術書マニア厳選】エンジニア必読、技術書おすすめ26選
エンジニアにおすすめの技術書 書籍学習は、エンジニアの嗜みみたいなところがありますが、 良書というものは、意外とそこまで多くもありません。 そこで本記事では「技術書マニアの筆者が厳選した技術書20選」 ...
続きを見る