Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyのメソッドルックアップパスを徹底解説!メソッド探索の仕組みを完全理解しよう

メソッドを呼び出したとき、Ruby内部では何が起きているのでしょうか?同名のメソッドが複数存在する場合、Rubyはどちらを呼び出すべきかをどう判断するのでしょう?そもそも、そのメソッドはどこに定義されているのでしょうか?

Rubyには、呼び出すべき正しいメソッドを決定し、該当するメソッドが見つからない場合に「NoMethodError」を返すタイミングを判断するための、明確な「仕組み」が備わっています。この仕組みこそが「Rubyメソッドルックアップパス(Method Lookup Path)」と呼ばれるものです。

本記事では、Rubyのメソッドルックアップについて詳しく掘り下げます。読み終える頃には、Rubyがオブジェクトの継承階層をどのようにたどって、目的のメソッドを特定しているのかをしっかりと理解できるようになるでしょう。

記事の内容を十分に理解するには、Rubyの基礎知識が必要です。モジュールやクラスにも触れますが、それらの詳細な解説は行いません。あくまで本記事のゴールである「Rubyがオブジェクトに渡されたメッセージ(メソッド)をどう特定するのか」を理解するために必要な範囲に絞って説明します。

概要

first_person.valid? のようなメソッド呼び出しを行うとき、Rubyはいくつかのことを判断しなければなりません。

  1. メソッド .valid? がどこで定義されているのか
  2. .valid? メソッドが複数箇所で定義されている場合、この文脈においてどれを使うべきか

Rubyがこれらを突き止めるためにたどる一連の手順(経路)こそがメソッドルックアップです。Rubyはメソッドが定義された場所を見つけ出し、それを呼び出します。正しいメソッドを確実に呼び出すため、Rubyは以下の場所を順番に検索します。

  1. シングルトンメソッド:Rubyでは、あるオブジェクト固有のメソッドを定義できます。これらのメソッドはそのオブジェクトだけで利用可能であり、同じクラスの他のインスタンスからはアクセスできません。
  2. ミックスインされたモジュール内のメソッド:モジュールは prependincludeextend を使ってクラスに取り込めます。取り込まれたクラスはモジュール内のメソッドにアクセスでき、Rubyは呼び出されたメソッドをモジュールの中から探します。また、モジュールにさらに別のモジュールがミックスインされている場合も、その先まで検索が続きます。
  3. インスタンスメソッド:クラス本体で定義され、そのクラスのインスタンスからアクセスできるメソッドです。
  4. 親クラスのメソッドやモジュール:クラスが別のクラスの子クラスである場合、Rubyは親クラスも検索します。検索対象には、親クラスのシングルトンメソッド、ミックスインされたモジュール、さらにその親クラスも含まれます。
  5. Object、Kernel、BasicObject:これらは最後に検索される場所です。Rubyのすべてのオブジェクトは、これらを祖先として持っているためです。

クラスとモジュール

メソッドは通常、オブジェクトに対して呼び出されます。これらのオブジェクトは、Ruby組み込みのクラスや開発者が作成したクラスから生成されます。次のようなクラスを見てみましょう。

class Human
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def hello
    puts "Hello! #{name}"
  end
end

このように定義した hello メソッドは、Human クラスのインスタンスに対して呼び出せます。

john = Human.new("John")
john.hello # 出力 -> Hello! John

hello メソッドはインスタンスメソッドなので、Human クラスのインスタンスから呼び出せるのです。一方で、インスタンスではなくクラスそのものに対して呼び出したいメソッドもあります。その場合はクラスメソッドを定義します。上記のクラスにクラスメソッドを追加すると、次のようになります。

def self.me
  puts "I am a class method"
end

これは Human.me のように呼び出せます。アプリケーションが複雑になるにつれて(新しいスタートアップを開発していると想像してください)、複数のクラスが同じ処理を行うメソッドを持つ場面が出てきます。そんなときはDRY原則に従い、コードの重複を避けたいところです。課題となるのは、複数のクラス間で機能をどう共有するかという点です。

モジュールを使ったことがない方なら、「共有用」のメソッドだけを集めた新しいクラスを作りたくなるかもしれません。しかし、それは望ましくない結果を招く可能性があります。特に、複数継承が必要になった場合、Rubyは単一継承しかサポートしていないため問題になります。こうしたケースに最適なのがモジュールです。モジュールはクラスによく似ていますが、いくつか違いがあります。まず、モジュールの例を見てみましょう。

module Movement
  def walk
    puts "I can walk!"
  end
end
  1. 定義は class ではなく module キーワードで始まる
  2. モジュールはインスタンス化できないため、Movement.new は使えない

メソッドとは

メソッドは、特定のオブジェクトが実行する「動作」と捉えることができます。例えば、変数 numberList に配列 [2, 3, 4] が代入されている場合、.push メソッドは配列が実行できる動作の一つで、受け取った値を配列に追加します。

john.walk

このようなコードを見ると、「オブジェクトのメソッドを呼び出している」と言いたくなりますよね。johnHuman クラスのインスタンスであるオブジェクトを参照し、walk はメソッドです。しかし、これは厳密には正しくありません。実際に呼び出されるメソッドは、オブジェクト自身ではなく、そのオブジェクトのクラススーパークラス、またはミックスインされたモジュールから来ていることが多いからです。

ここで重要な点として、オブジェクトに直接メソッドを定義することも可能です。Rubyではすべてがオブジェクトであり、オブジェクトを生成するためのクラスでさえオブジェクトだからです。

def john.drip
  puts "My drip is eternal"
end

この drip メソッドは、変数 john に代入されたオブジェクトからのみアクセスできます。このようなメソッドはシングルトンメソッドと呼ばれ、john オブジェクト専用のものになります。ちなみに、シングルトンメソッドとクラスメソッドに本質的な違いはありません。上記のようなオブジェクトに直接定義されたメソッドのことを指しているのでなければ、「メソッドは特定のオブジェクトに属する」と表現するのは不正確です。先ほどの例で言えば、walk メソッドは Movement モジュールに属し、hello メソッドは Human クラスに属しています。この理解があれば、さらに一歩進んだ話も容易につかめるでしょう。つまり、オブジェクトに対して呼び出されたメソッドを正確に特定するために、Rubyはオブジェクトのクラス、スーパークラス、そしてオブジェクトの階層にミックスインされたモジュールを確認する必要があるのです。

モジュールのミックスイン

Rubyは単一継承のみをサポートしており、1つのクラスが継承できるのは1つのクラスだけです。子クラスは親クラスの振る舞い(メソッド)を受け継げますが、複数のクラス間で共有したい振る舞いがある場合はどうすればよいのでしょうか?

例えば、walk メソッドを Human クラスのインスタンスでも使えるようにするには、Movement モジュールを Human クラスにミックスインします。include を使って Human クラスを書き直すと、次のようになります。

require "movement" # movement.rb というファイルにモジュールがあると仮定

class Human
  include Movement

  attr_reader :name

  def initialize(name)
    @name = name
  end

  def hello
    puts "Hello! #{name}"
  end
end

これで、インスタンスから walk メソッドを呼び出せるようになりました。

john = Human.new("John")
john.walk

include

include キーワードを使うと、取り込んだモジュールのメソッドがクラスのインスタンスメソッドとして追加されます。これは、includeされたモジュールが Human クラスの祖先(ancestors)チェーンに挿入されるためです。イメージとしては、Movement モジュールが Human クラスの親のような位置に置かれます。前述の例でも、Human クラスのインスタンスに対して walk メソッドを呼び出せていますね。

extend

include に加えて、Rubyには extend キーワードも用意されています。こちらはモジュールのメソッドをクラスメソッド(前述のとおり、シングルトンメソッドとも呼ばれます)として利用可能にします。例えば、次のような Feeding モジュールがあるとしましょう。

module Feeding
  def food
    "I make my food :)"
  end
end

このモジュールを require して extend Feeding を追加すれば、Human クラスでこの振る舞いを共有できます。ただし、使い方がポイントです。インスタンスに対して food メソッドを呼び出すのではなく、クラスメソッドと同じようにクラスそのものに対して呼び出します。

Human.food

prepend

prependinclude と似ていますが、重要な違いがあります。

prependはincludeと同じように機能しますが、モジュールをクラスとそのスーパークラスの間に挿入するのではなく、祖先チェーンの最下部、つまりクラス自身よりも前に挿入する点が異なります。

つまり、クラスのインスタンスに対してメソッドを呼び出したとき、Rubyはクラス内のメソッドよりも先に、prependされたモジュールのメソッドを探すということです。

例えば、hello メソッドを定義したモジュールを prependHuman クラスにミックスインした場合、Rubyはクラス内の hello メソッドではなく、モジュール内の hello メソッドを呼び出します。

prepend の動作をより深く理解したい方は、関連記事を参照することをおすすめします。

メソッドルックアップパス

Rubyインタプリタがメソッド呼び出し時に最初に確認するのは、シングルトンメソッドです。実際にコードを実行して結果を試してみると、理解が深まります。

ここで、次のようなモジュールとクラスがいくつかあると仮定しましょう。

module One
  def another
    puts "From one module"
  end
end

module Two
  def another
    puts "From two module"
  end
end

module Three
  def another
    puts "From three module"
  end
end

class Creature
  def another
    puts "From creature class"
  end
end

これらを Human クラスにミックスインしてみましょう。

class Human < Creature
  prepend Three
  extend Two
  include One

  def another
    puts "Instance method"
  end

  def self.another
    puts "From Human class singleton"
  end
end

モジュールのミックスインに加えて、インスタンスメソッドとクラスメソッドも定義しました。また、Human クラスは Creature クラスのサブクラスであることにも注目してください。

最初の探索:シングルトンメソッド

Human.another を実行すると、出力されるのは From Human class singleton です。これはクラスメソッドとして定義した内容ですね。このクラスメソッドをコメントアウトして再実行すると、今度は From two module が出力されます。これは extend でミックスインしたモジュール由来のものです。つまり、ルックアップはシングルトンメソッドから始まることが分かります。さらに extend Two も削除(コメントアウト)して再実行すると、今度はNoMethodError(メソッドが見つからないエラー)が発生します。これは、Rubyがシングルトンメソッドの中に another メソッドを見つけられなかったためです。

次に、インスタンスを作成してみましょう。

n = Human.new

そして、このインスタンスにシングルトンメソッドを定義します。

def n.another
  puts "From n object"
end

この状態で n.another を実行すると、呼び出されるのは n オブジェクトに定義したシングルトンメソッドです。extend でミックスインしたモジュールのメソッドが呼ばれないのは、メソッドをクラスのインスタンスに対して呼び出しているからです。重要なのは、シングルトンメソッドは extend によるモジュールのメソッドよりも高い優先順位を持つという点です。

2番目の探索:prepend でミックスインされたモジュール

n オブジェクトのシングルトンメソッドをコメントアウトして再実行すると、今度は prepend でミックスインしたモジュールのバージョンが呼び出されます。これは、prepend がモジュールをクラス自身より手前に挿入するためです。

3番目の探索:クラス本体

モジュール Three をコメントアウトすると、呼び出されるのはクラスに定義されたインスタンスメソッドのバージョンです。

4番目の探索:include でミックスインされたモジュール

Rubyが次にメソッドを探すのは、include でミックスインされたモジュールの中です。インスタンスメソッドをコメントアウトすると、今度はモジュール One 内のバージョンが返されます。

5番目の探索:親クラス

クラスに親クラスが存在する場合、Rubyは親クラスも検索します。この検索には、親クラスにミックスインされたモジュールも含まれます。もし Creature クラスにミックスインされたモジュール内でメソッドが定義されていれば、そのメソッドが呼び出されるのです。

メソッド探索の終わり

メソッドの探索がどこで終わるのかは、クラスに対して .ancestors を呼び出して祖先チェーンを確認すれば分かります。Human クラスで実行すると、[Three, Human, One, Creature, Object, Kernel, BasicObject] が返されます。メソッドの探索は、Rubyのルートクラスである BasicObject クラスで終わります。何らかのクラスのインスタンスであるすべてのオブジェクトは、BasicObject クラスに起源を持ちます。

メソッド探索が開発者が定義した親クラスを過ぎると、以下のクラス・モジュールに到達します。

  • Object クラス
  • Kernel モジュール
  • BasicObject クラス

method_missing メソッド

Rubyを使っていると、NoMethodError に遭遇したことがあるでしょう。これは、オブジェクトに対して未知のメソッドを呼び出そうとしたときに発生します。このエラーは、Rubyがオブジェクトの祖先チェーン全体を調べてもメソッドを見つけられなかった後に発生します。表示されるエラーメッセージは、BasicObject クラスで定義されている method_missing メソッドによって処理されています。このメソッドはオーバーライドすることも可能で、独自の動作を実装することもできます。

まとめ

これで、Rubyがオブジェクトに対して呼び出されたメソッドを特定するまでの道筋を理解できました。この知識があれば、未知のメソッドを呼び出したことで発生するエラーにも、落ち着いて対処できるようになるはずです。ぜひ実際にコードを書きながら、メソッドルックアップパスの挙動を確かめてみてください。

  1. Rubyのgsubメソッド徹底解説!正規表現・ブロック・ハッシュで使いこなす3つのテクニック

    今回はRubyのgsubメソッドについて、その使い方を詳しく解説していきます。gsubを試すには、まず操作対象となる文字列が必要です。 なぜなら、gsubの目的は文字列の一部を別の文字列に置き換えることだからです。 実は、「gsub」の「sub」は「substitute(置換)」、「g」は「global(全体)」を意味しています。つまり、文字列内に現れる該当箇所をすべて置換するメソッドなのです。 例として、次のような文字列を用意しました: str = white chocolate ここで「white」という単語を「dark」に置き換えたいとしましょう。 その方法がこちらです: str.gs

  2. Rubyのfreezeメソッド完全解説 – オブジェクトの可変性と不変性を理解しよう

    オブジェクトが「変更可能(ミュータブル)」であるとは、どういう意味なのでしょうか? 難しい言葉に構える必要はありません。「可変性(ミュータビリティ)」とは、単純に「オブジェクトの内部状態を後から変更できる」という意味です。これはすべてのオブジェクトのデフォルトの挙動であり、freeze(凍結)されたオブジェクトや、言語側で特別扱いされている一部のオブジェクトだけが例外となります。 つまり、Rubyのすべてのオブジェクトが変更可能というわけではないのです。 なぜ数値やシンボルは変更できないのか? たとえば、整数・シンボル、さらにはtrueやfalse(これらもすべてオブジェクトです)が変化するの