Rubyメタプログラミングは思ったよりもクールです
公開: 2022-03-11メタプログラミングはRubyの忍者だけが使用するものであり、一般の人間向けではないということをよく耳にします。 しかし、真実は、メタプログラミングはまったく怖いものではないということです。 このブログ投稿は、この種の考え方に挑戦し、メタプログラミングを平均的なRuby開発者に近づけて、そのメリットを享受できるようにするのに役立ちます。
メタプログラミングは多くのことを意味する可能性があり、使用法に関しては非常に誤用されて極端になる可能性があることに注意してください。そこで、誰もが日常のプログラミングで使用できる実例をいくつか紹介します。
メタプログラミング
メタプログラミングは、実行時にそれ自体で動的にコードを書き込むコードを記述できる手法です。 これは、実行時にメソッドとクラスを定義できることを意味します。 クレイジーだよね? 簡単に言うと、メタプログラミングを使用して、クラスを再度開いて変更したり、存在しないメソッドをキャッチしてその場で作成したり、繰り返しを避けてDRYであるコードを作成したりできます。
基礎
本格的なメタプログラミングに飛び込む前に、基本を探る必要があります。 そして、それを行うための最良の方法は例によるものです。 1つから始めて、Rubyメタプログラミングを段階的に理解しましょう。 あなたはおそらくこのコードが何をしているのか推測することができます:
class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" end end
2つのメソッドでクラスを定義しました。 このクラスの最初のメソッドはクラスメソッドで、2番目のメソッドはインスタンスメソッドです。 これはRubyの基本的なことですが、このコードの背後にはさらに多くのことが起こっており、先に進む前に理解する必要があります。 クラスDeveloper
自体が実際にはオブジェクトであることを指摘する価値があります。 Rubyでは、クラスを含むすべてがオブジェクトです。 Developer
はインスタンスであるため、クラスClass
のインスタンスです。 Rubyオブジェクトモデルは次のようになります。
p Developer.class # Class p Class.superclass # Module p Module.superclass # Object p Object.superclass # BasicObject
ここで理解しておくべき重要なことの1つは、 self
の意味です。 frontend
メソッドは、クラスDeveloper
のインスタンスで使用できる通常のメソッドですが、 backend
メソッドがクラスメソッドであるのはなぜですか? Rubyで実行されるすべてのコードは、特定の自己に対して実行されます。 Rubyインタープリターがコードを実行すると、常に任意の行の値self
を追跡します。 self
は常に何らかのオブジェクトを参照していますが、そのオブジェクトは実行されたコードに基づいて変更される可能性があります。 たとえば、クラス定義内では、 self
はクラスClass
のインスタンスであるクラス自体を参照します。
class Developer p self end # Developer
インスタンスメソッド内では、 self
はクラスのインスタンスを参照します。
class Developer def frontend self end end p Developer.new.frontend # #<Developer:0x2c8a148>
クラスメソッド内では、 self
はある意味でクラス自体を参照します(これについては、この記事の後半で詳しく説明します)。
class Developer def self.backend self end end p Developer.backend # Developer
これは問題ありませんが、結局のところ、クラスメソッドとは何ですか? その質問に答える前に、メタクラスと呼ばれるものの存在について言及する必要があります。これは、シングルトンクラスおよび固有クラスとしても知られています。 以前に定義したクラスメソッドfrontend
は、オブジェクトDeveloper
のメタクラスで定義されたインスタンスメソッドに他なりません。 メタクラスは基本的に、Rubyが作成し、継承階層に挿入してクラスメソッドを保持するクラスであるため、クラスから作成されたインスタンスに干渉することはありません。
メタクラス
Rubyのすべてのオブジェクトには独自のメタクラスがあります。 どういうわけか開発者には見えませんが、そこにあり、非常に簡単に使用できます。 私たちのクラスDeveloper
は本質的にオブジェクトであるため、独自のメタクラスがあります。 例として、クラスString
のオブジェクトを作成し、そのメタクラスを操作してみましょう。
example = "I'm a string object" def example.something self.upcase end p example.something # I'M A STRING OBJECT
ここで行ったsomething
は、オブジェクトにシングルトンメソッドを追加したことです。 クラスメソッドとシングルトンメソッドの違いは、クラスメソッドはクラスオブジェクトのすべてのインスタンスで使用できるのに対し、シングルトンメソッドはその単一のインスタンスでのみ使用できることです。 クラスメソッドは広く使用されていますが、シングルトンメソッドはそれほど多くはありませんが、両方のタイプのメソッドがそのオブジェクトのメタクラスに追加されます。
前の例は次のように書き直すことができます。
example = "I'm a string object" class << example def something self.upcase end end
構文は異なりますが、事実上同じことを行います。 ここで、 Developer
クラスを作成した前の例に戻り、クラスメソッドを定義するために他のいくつかの構文を調べてみましょう。
class Developer def self.backend "I am backend developer" end end
これは、ほとんどすべての人が使用する基本的な定義です。
def Developer.backend "I am backend developer" end
これは同じことですDeveloper
のbackend
クラスメソッドを定義しています。 self
は使用しませんでしたが、このようにメソッドを定義すると、事実上クラスメソッドになります。
class Developer class << self def backend "I am backend developer" end end end
ここでも、クラスメソッドを定義していますが、 String
オブジェクトのシングルトンメソッドを定義するために使用したものと同様の構文を使用しています。 ここでは、 Developer
オブジェクト自体を参照するself
を使用していることに気付くかもしれません。 まず、 Developer
クラスを開き、selfをDeveloper
クラスと等しくしました。 次に、 class << self
を実行し、selfをDeveloper
のメタクラスと等しくします。 次に、 Developer
のメタクラスでメソッドbackend
を定義します。
class << Developer def backend "I am backend developer" end end
このようにブロックを定義することで、ブロックの期間中、 Developer
のメタクラスにself
を設定します。 その結果、 backend
メソッドは、クラス自体ではなく、 Developer
のメタクラスに追加されます。
このメタクラスが継承ツリーでどのように動作するかを見てみましょう。
前の例で見たように、メタクラスが存在するという本当の証拠はありません。 しかし、この目に見えないクラスの存在を示すことができる小さなハックを使用することができます:
class Object def metaclass_example class << self self end end end
Object
クラスでインスタンスメソッドを定義すると(はい、いつでも任意のクラスを再開できます。これはメタプログラミングのもう1つの利点です)、その中のObject
オブジェクトをself
参照することができます。 次に、 class << self
構文を使用して、現在のオブジェクトのメタクラスを指すように現在の自己を変更できます。 現在のオブジェクトはObject
クラス自体であるため、これはインスタンスのメタクラスになります。 このメソッドは、この時点でメタクラス自体であるself
を返します。 したがって、任意のオブジェクトでこのインスタンスメソッドを呼び出すことにより、そのオブジェクトのメタクラスを取得できます。 Developer
クラスをもう一度定義して、少し調べてみましょう。
class Developer def frontend p "inside instance method, self is: " + self.to_s end class << self def backend p "inside class method, self is: " + self.to_s end end end developer = Developer.new developer.frontend # "inside instance method, self is: #<Developer:0x2ced3b8>" Developer.backend # "inside class method, self is: Developer" p "inside metaclass, self is: " + developer.metaclass_example.to_s # "inside metaclass, self is: #<Class:#<Developer:0x2ced3b8>>"
そして、クレッシェンドについては、 frontend
がクラスのインスタンスメソッドであり、 backend
がメタクラスのインスタンスメソッドであるという証明を見てみましょう。

p developer.class.instance_methods false # [:frontend] p developer.class.metaclass_example.instance_methods false # [:backend]
ただし、メタクラスを取得するために、実際にObject
を再度開いて、このハックを追加する必要はありません。 Rubyが提供するsingleton_class
を使用できます。 これは、追加したmetaclass_example
と同じですが、このハックを使用すると、Rubyが内部でどのように機能するかを実際に確認できます。
p developer.class.singleton_class.instance_methods false # [:backend]
「class_eval」と「instance_eval」を使用したメソッドの定義
クラスメソッドを作成するもう1つの方法があります。それは、 instance_eval
を使用することです。
class Developer end Developer.instance_eval do p "instance_eval - self is: " + self.to_s def backend p "inside a method self is: " + self.to_s end end # "instance_eval - self is: Developer" Developer.backend # "inside a method self is: Developer"
このコードのRubyインタープリターは、インスタンス(この場合はDeveloper
オブジェクト)のコンテキストで評価します。 また、オブジェクトでメソッドを定義する場合は、クラスメソッドまたはシングルトンメソッドのいずれかを作成します。 この場合、これはクラスメソッドです。正確には、クラスメソッドはシングルトンメソッドですが、クラスのシングルトンメソッドであり、その他はオブジェクトのシングルトンメソッドです。
一方、 class_eval
は、インスタンスではなくクラスのコンテキストでコードを評価します。 それは実質的にクラスを再開します。 class_eval
を使用してインスタンスメソッドを作成する方法は次のとおりです。
Developer.class_eval do p "class_eval - self is: " + self.to_s def frontend p "inside a method self is: " + self.to_s end end # "class_eval - self is: Developer" p developer = Developer.new # #<Developer:0x2c5d640> developer.frontend # "inside a method self is: #<Developer:0x2c5d640>"
要約すると、 class_eval
メソッドを呼び出すときは、元のクラスを参照するようにself
を変更し、 instance_eval
を呼び出すときは、元のクラスのメタクラスを参照するようにself
を変更します。
欠落しているメソッドをその場で定義する
メタプログラミングパズルのもう1つのピースは、 method_missing
です。 オブジェクトのメソッドを呼び出すと、Rubyは最初にクラスに入り、そのインスタンスメソッドを参照します。 そこにメソッドが見つからない場合は、祖先チェーンの検索を続行します。 それでもRubyがメソッドを見つけられない場合は、 method_missing
という名前の別のメソッドを呼び出します。これは、すべてのオブジェクトが継承するKernel
のインスタンスメソッドです。 Rubyは最終的に不足しているメソッドに対してこのメソッドを呼び出すと確信しているので、これを使用していくつかのトリックを実装できます。
define_method
は、メソッドを動的に作成するために使用できるModule
クラスで定義されたメソッドです。 define_method
を使用するには、新しいメソッドの名前と、ブロックのパラメーターが新しいメソッドのパラメーターになるブロックを使用して呼び出します。 def
を使用してメソッドを作成することとdefine_method
を使用することの違いは何ですか? define_method
をmethod_missing
と組み合わせて使用してDRYコードを記述できることを除いて、大きな違いはありません。 正確には、クラスを定義するときにdef
の代わりにdefine_method
を使用してスコープを操作できますが、それはまったく別の話です。 簡単な例を見てみましょう。
class Developer define_method :frontend do |*my_arg| my_arg.inject(1, :*) end class << self def create_backend singleton_class.send(:define_method, "backend") do "Born from the ashes!" end end end end developer = Developer.new p developer.frontend(2, 5, 10) # => 100 p Developer.backend # undefined method 'backend' for Developer:Class (NoMethodError) Developer.create_backend p Developer.backend # "Born from the ashes!"
これは、 def
を使用せずにdefine_method
を使用してインスタンスメソッドを作成する方法を示しています。 しかし、私たちがそれらを使ってできることはもっとたくさんあります。 このコードスニペットを見てみましょう。
class Developer def coding_frontend p "writing frontend" end def coding_backend p "writing backend" end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"
このコードはDRYではありませんが、 define_method
を使用してDRYにすることができます。
class Developer ["frontend", "backend"].each do |method| define_method "coding_#{method}" do p "writing " + method.to_s end end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"
それははるかに優れていますが、それでも完璧ではありません。 なんで? たとえば、新しいメソッドcoding_debug
を追加する場合は、この"debug"
を配列に配置する必要があります。 しかし、 method_missing
を使用すると、これを修正できます。
class Developer def method_missing method, *args, &block return super method, *args, &block unless method.to_s =~ /^coding_\w+/ self.class.send(:define_method, method) do p "writing " + method.to_s.gsub(/^coding_/, '').to_s end self.send method, *args, &block end end developer = Developer.new developer.coding_frontend developer.coding_backend developer.coding_debug
このコードは少し複雑なので、分解してみましょう。 存在しないメソッドを呼び出すと、 method_missing
が起動します。 ここでは、メソッド名が"coding_"
で始まる場合にのみ新しいメソッドを作成します。 それ以外の場合は、superを呼び出して、実際に欠落しているメソッドを報告する作業を行います。 そして、単にdefine_method
を使用してその新しいメソッドを作成しています。 それでおしまい! このコードを使用すると、 "coding_"
で始まる文字通り何千もの新しいメソッドを作成できます。そのため、コードはDRYになります。 define_method
はたまたまModule
に対してプライベートであるため、 send
を使用して呼び出す必要があります。
まとめ
これは氷山の一角にすぎません。 Ruby Jediになるには、これが出発点です。 メタプログラミングのこれらの構成要素を習得し、その本質を真に理解した後、たとえば、独自のドメイン固有言語(DSL)を作成するなど、より複雑なことに進むことができます。 DSLはそれ自体がトピックですが、これらの基本的な概念は、高度なトピックを理解するための前提条件です。 Railsで最もよく使用されるgemのいくつかはこの方法で構築されており、RSpecやActiveRecordなど、知らないうちにDSLを使用した可能性があります。
この記事が、メタプログラミングの理解に一歩近づき、さらには、より効率的にコーディングするために使用できる独自のDSLを構築することさえできることを願っています。