Rubyメタプログラミングは思ったよりもクールです

公開: 2022-03-11

メタプログラミングはRubyの忍者だけが使用するものであり、一般の人間向けではないということをよく耳にします。 しかし、真実は、メタプログラミングはまったく怖いものではないということです。 このブログ投稿は、この種の考え方に挑戦し、メタプログラミングを平均的な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オブジェクトモデルは次のようになります。

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

これは同じことですDeveloperbackendクラスメソッドを定義しています。 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_methodmethod_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を構築することさえできることを願っています。