Ruby
メタプログラミング

Rubyのオブジェクト、クラス、メソッド探索 (メタプログラミングRuby勉強会)

はじめに

RESTAR株式会社では、Fintech関連のBtoBサービスをRuby on Railsで開発しています。普段の業務で使っているRubyをより深く理解するため、メタプログラミングRubyを題材に社内勉強会を行いました。これは、勉強会まとめの第一弾です。

対象読者

この記事では、以下の方々を対象に書いています。

  • Rubyを初めて触る人
  • Rubyを触ったことはあるが、Rubyの言語仕様に関して詳しく知らない人

Rubyとは

Wikipedia先生は

Ruby(ルビー)は、まつもとゆきひろ(通称 Matz)により開発されたオブジェクト指向スクリプト言語であり、スクリプト言語が用いられてきた領域でのオブジェクト指向プログラミングを実現する。 また日本で開発されたプログラミング言語としては初めて国際電気標準会議で国際規格に認証された事例となった[4]。 (Wikipedia Rubyより)

とおっしゃっています。つまり、Rubyとはオブジェクト指向スクリプト言語であり、かつ初めて世界的に認められた日本製の言語です(←適当)。

また、開発者のまつもとゆきひろさんは、「Rubyの言語仕様策定において最も重視しているのはストレスなくプログラミングを楽しむことである (enjoy programming)」と述べており(wikipedia Ruby)、実際にRubyは良くも悪くも非常に柔軟な言語であり、短いコードで驚くほど多くの機能を実装することができます。

Rubyは良くも悪くも柔軟

先程、Rubyは"良くも悪くも"柔軟な言語であると書きました。良い点としては、Rubyの機能を活用すれば、メタプログラミングを簡単に行うことができるようになり、驚くほど短いコードでいくつものメソッドを動的に追加できたり、独自のDSLをサクッと作ることができます。
また、Rubyではカプセル化の破壊や、プライベートメソッドをどこからでも呼ぶことができます!!!その結果、下手にRubyを書くと破綻したコードができあがってしまいます。Rubyの柔軟性を正しく利用するためには、Rubyに対して正しく理解した上でコードを書く必要があるのです。

ちょっと例を見てみましょう。例えば、次のようなto_alphanumericメソッドがあったとします。

def to_alphanumeric(s)
  s.gsub(/[^\w\s]/, '')
end

見てわかるように、to_alphanumericメソッドは与えられた文字列のアルファベットとスペースだけ残して、他の文字は削除するメソッドです。一見、良さそうに思えるこのメソッドですが、オブジェクト指向的に考えると、文字列に汎用的に使えるメソッドなので、「Stringクラスに持たせても良いのではないか?」と考えられます。では、実際に標準のStringクラスへメソッド追加をしてみましょう。

'h*o&g.e'.to_alphanumeric # => NoMethodError

class String
  def to_alphanumeric
    gsub(/[^\w\s]/, '')
  end
end

'h*o&g.e'.to_alphanumeric # => 'hoge'

そうです。単にStringクラスを再オープンして、新たにメソッドを追加するだけでよいのです。標準クラスにメソッドを追加するなんて、難しそうに聞こえますが実際はとても簡単ですよね?この技法をオープンクラスと言います。ですが、このオープンクラス、副作用もあります。例えば使用しているgemのクラスを知らないうちに手元で書き換えてしまったときに、思わぬ動作に苦しむことになるかもしれません。

ただ、その一方でRefinementsを利用すれば、安全にStringへのメソッド追加を行うことができます。

module StringExtensions 
  refine String do
    def to_alphanumeric 
      gsub(/[^\w\s]/, '')
    end 
  end
end

'h*o&g.e'.to_alphanumeric # => NoMethodError
using StringExtensions
'h*o&g.e'.to_alphanumeric # => 'hoge'

Refinementsを行うには、例のようにModule内でrefineを呼び出し、Stringto_alphanumericを追加することで実現できます。また、Refinementsで追加したメソッドを有効にするには、例のようにusingを使うことで有効にすることができる。

このように、Rubyは非常に柔軟な言語であるのと同時に、柔軟すぎるがゆえに思わぬ副作用を引き起こしてしまう可能性があります。また、Refinementsのように副作用を抑えることができる機能も存在します。そのため、Rubyについて理解を深めることで、思わぬバグを発生させることを減らすことができるようになります。

Rubyへの理解の重要性は少しはご理解いただけたでしょうか?では、本題のRubyの言語仕様について紹介していきます。

Rubyの登場人物は、”ほぼ”全てオブジェクト

正確には、メソッドとブロック等の例外はありますが、Rubyのほとんどの登場人物はオブジェクトです。では、オブジェクトとは何でしょうか? とある辞典 には「オブジェクト指向プログラミングにおいて,内部構造をもつデータ」と説明されています。具体例を見てみましょう。

class MyClass 
  def initialize
    @fuga = 'aaa'
  end

  def hoge
    pp @fuga
  end
end

obj = MyClass.new
obj.class # => MyClass
obj.hoge # => 'aaa'

上の例の場合、MyClassクラスのnewメソッドを呼び出すことでMyClassクラスのオブジェクトを生成することができます。newメソッドが呼び出すと、initializeメソッドが呼び出され、ここでは@fugaというインスタンス変数がオブジェクトにセットされます。
また、objMyClassクラス内で定義したhugaメソッドを呼び出すことができるようになります。ちなみに、@hugaobjに属し、hogeメソッドはMyClassクラスに属します。
image.png

このようにインスタンス変数やどのクラスに紐付いているか等の内部情報を持つデータがRubyのオブジェクトです。

クラスもオブジェクト

他の言語をかじったことがある人は驚くかもしれませんが、上の例で登場したMyClassや、クラスも実はオブジェクトです!実際に見てみましょう。オブジェクトのクラスを調べるには、classメソッドを使います。先程のMyClassからclassメソッドを使って、さかのぼってみます。

obj = MyClass.new
obj.class # => MyClass
MyClass.class # => Class
Class.class # => Class

例を見ていくと、objオブジェクトのクラスは、MyClassクラスであることがわかります。次に、MyClassクラスのクラスがClassクラスであることがわかります。つまり、MyClassクラスはClassクラスのオブジェクトです。さらにClassクラスのクラスはClassクラスです!これは、Rubyの実装として自己参照をしているため、このようになっているようです。よく考えると分かるのですが理解にちょっと時間がかかりました... :sweat_drops:

これ以外にも、先程オブジェクトではないと説明したメソッドやブロックも、Objectクラスのmethodメソッドを使えば、メソッドをオブジェクトにすることができますし、Proclambdaを使えばブロックをオブジェクト化することができます😂

他にも面白い特徴があります。(とても当たり前なのですが)全てのオブジェクトの継承をたどっていくとObjectクラスにたどり着きます。superclassを使うとクラスの親クラスを確認することができます。MyClassクラスを実際にさかのぼってみましょう。

MyClass.superclass # => Object
Object.superclass # => BasicObject
BasicObject.superclass # => nil

このように、MyClassをさかのぼるとObjectクラスにたどり着きます。MyClass以外にClassModuleArrayHashなども継承関係をさかのぼっていくとObjectクラスへたどり着きます。これが、これが、Rubyの登場人物は(ほぼ)全てオブジェクトであると言った所以です。面白いですねー

※ ちなみにObjectクラスの親クラスであるBasicObjectクラスは、全てのクラス階層のルートであり、必要最低限のメソッドしかもたない、いわゆる白紙のクラスみたいなものです。

メソッド呼び出し

先の例に戻ります。

class MyClass 
  def initialize
    @fuga = 'aaa'
  end

  def hoge
    pp @fuga
  end
end

obj = MyClass.new
obj.class # => MyClass
obj.hoge # => 'aaa'

先程、objhogeメソッドを呼び出せると言いました。Rubyでは、オブジェクトのクラスを探索してメソッドを発見しています。これをメソッド探索といいます。その後、メソッドを実行します。

メソッド探索

最初にメソッド探索に関して説明します。

メソッド探索はメタプログラミングRubyでは、「Rubyがレシーバのクラスに入り、メソッドを見つけるまで継承チェーンを上ること」と説明されています。すなわち、オブジェクトに紐付いているクラスにメソッドを探しに行き、見つからなければ親クラスを探しに行きます。これをメソッドが見つかるまで、さかのぼっていきます。

image.png

この継承チェーンですが、ancestorsメソッドで確認することができます。

obj = MyClass.new
obj.ancestors # => [MyClass, Object, Kernel, BasicObject]

上の例を見ると、MyClass->Object->Kernel->BasicObjectの順でメソッド探索を行っていることがわかります。ここで、superclassメソッドを使って継承をたどっていった時にはなかったKernelが登場していることがわかります。このKernelは何者なのでしょうか?

Moduleのメソッド探索

このKernelはモジュールです。先程、メソッド探索は「Rubyがレシーバのクラスに入り、メソッドを見つけるまで継承チェーンを上ること」と書きましたが、この継承チェーンにはモジュールも含めることができます。

先の継承の種明かしをするとObjectKernelモジュールを継承チェーンに追加しています。では、どのようにモジュールを継承チェーンに追加するのでしょうか?モジュールを継承チェーンに取り込むためには、includeもしくはprependを使います。

class MyClass 
  include A
  prepend B
end

obj = MyClass.new
obj.ancestors # => [B, MyClass, A, Object, Kernel, BasicObject]

この例のように、includeを使うとクラスの後ろにモジュールを継承チェーンに追加し、prependを使うとクラスの前にモジュールを継承チェーンに追加します。このようにして、モジュールを継承チェーンに取り組み、モジュール内のメソッドも探索できるようになります。

メソッドの実行

メソッド探索でメソッドが見つかったら次はメソッドの実行です。また、先の例に戻ります。

class MyClass 
  def initialize
    @fuga = 'aaa'
  end

  def hoge
    pp @fuga
  end
end

obj = MyClass.new
obj.hoge # => 'aaa'

objhogeメソッドを呼び出すと、メソッド探索でMyClasshogeメソッドがヒットします。hogeメソッドでは、インスタンス変数である@fugaをコンソールに出力しています。では、この@fugaは、どのオブジェクトのインスタンス変数でしょうか?
先程、インスタンス変数はオブジェクトに属すると説明しました。つまり、メソッドの呼び出し元であるobj(このオブジェクトをレシーバといいます。)のインスタンス変数を使って、メソッドの実行を行います。

まとめ

今までの話をまとめると次のようになります。

オブジェクト

  • Rubyのほぼ全ての登場人物はオブジェクトである
  • クラスもオブジェクトである
  • オブジェクト化可能なものは、継承関係をたどっていくと必ずBaseObjectにたどり着く

メソッド呼び出し

  1. オブジェクトがメソッドを呼び出す
  2. 継承チェーンをたどって、オブジェクトが呼び出しているメソッドを探す
  3. メソッドが見つかったら、オブジェクトのインスタンス変数を使って実行する

以上です。他にも、モジュールが絡んだ時のメソッド探索や、レシーバに関して詳しく話したいと思ったのですが、それはまた今度時間がある時に書きたいと思います。

次回は、メソッドに関して書きたいと思います。

何かお気づきの点がございましたら、コメントください!