webエンジニアの日常

RubyやPython, JSなど、IT関連の記事を書いています

名前の森で定数を見つける物語

Effective Ruby勉強メモです。

Rubyで大きなアプリケーションなどを作る場合には、どうしても既存クラスと同じ名前のクラスを定義したいときがあります。

以下のように書くとクラスを新しく定義しているかのように見えますが、実際には既存のArrayクラスを再オープンしてメソッドを追加してるだけにすぎません。

Rubyではクラスはミュータブル(可変)なのです。

class Array
  def print_first
    p first
  end
end

これでは思わぬ副作用をもたらしてしまいます。最悪、メソッドを完全に上書きしてしまいシステム全体に不具合が生じてしまいます。

そこで、新しくモジュールを定義し、その中でクラス定義をすることで既存のクラスと同じ名前の異なるクラスを定義することができます。これは名前空間と言われるテクニックです。

module Demo
  class Array
    def print_first
      p "first"
    end
  end
end

a = Demo::Array.new
a.print_first #=> "first"

呼び出す際はDemo::Arrayのようにモジュールを明示的に書くのが望ましいです。

呼び出すときにRubyがどのように定数を探すのかは後述します。

スポンサーリンク

モジュールを名前空間として使う

モジュールは任意の階層作ることができるので、いくらでも深くすることができます。

module A
  module B
    module C
      class D
      end
    end
  end
end

このとき、クラスDはA::B::C::Dというように呼び出します。

また、一般的にはモジュールの階層とディレクトリの階層を同じにするのが普通です。

Demo::Arrayの例では、demo/array.rbとし、A::B::C::Dでは、a/b/c/d.rbとします。

すでにモジュールが定義されている場合であれば、

class Demo::String
end

のように直接階層を書くこともできます。

これによって、無駄なインデントを省くことができます。

名前空間はクラスやモジュールだけのものではなく、定数に対しても名前空間に属することができます。(というか、クラスも定数なので、正確には名前空間は定数を一意に識別するために使います)

Railsを触ったことがある方なた、自分で作ったクラス内で定義した定数を呼び出すときは以下のように書くのをご存知かと思います。

class User
  KEY = "users_key"
end

User::KEY

グローバル名前空間

では、何も前につかないクラスなどはどこに存在しているのでしょうか。

実はトップレベル(どの階層の下にも含まれない)のクラスやモジュールは、Objectクラスに含まれています。

すなわち、Arrayクラスは何もつけなくても使えるクラスですが、Objectクラスに含まれているため、Object::Arrayとしても同様のクラスが呼び出されます。

このように修飾(Demo::のようなもの)を付けなくても使えるクラスはグローバル名前空間に属すると言われます。

グローバル名前空間に属するクラスは普通そのまま使いますが、何かしらモジュールの内部で呼び出す場合やあいまいになる場合は、Object::Array::Arrayと呼び出します。一般的には後者を用います。

定数の見つけ方

では、定数(ここではクラスやクラス内で定義された定数両方を含みます)を呼び出したときに、Rubyがどのように定数を探し出すかを見ていきます。

メソッド探索では継承階層を遡っていくだけですが、定数を探す場合にはその前に探す場所があります。

www.uosansatox.biz

それはレキシカルスコープと言われているものです。

  • レキシカルスコープ

文章だけで説明するのは難しいのですが、モジュールやクラスが定義されているスコープのことです。

例えば、以下は一つのレキシカルスコープです。

module User
  KEY = "users_key"
end

次のモジュールは定義されてから、再オープンされて別の定数を定義していますが、それらは別々のレキシカルスコープに属しています

module User
  KEY = "users_key"
end

module User
  PASS = "users_password"
end

KEYとPASSは同じモジュールに定義されていますが、別々のレキシカルスコープに属しています。

話を戻しますが、Rubyはまず現在の(呼び出した場所の)レキシカルスコープとその中に含まれるすべてのレキシカルスコープの中から探索を行います。

もしその中で見つからなければ継承階層の中から定数を探します。そのため、親クラスの定数が子クラスから参照可能となるのです。

module User
  KEY = "users_key"

  class Car
    def print_key
      p KEY
    end
  end
end

この例では、KEY定数が呼び出された場所はCarクラスであり、KEY定数が定義された場所と同じレキシカルスコープに入っています。

なのでCarクラスからKEY定数は呼び出せます。

以下の例はどうでしょうか

module User

  class Car
    KEY = "car_key"
  end

  class House
    def car_key
      p KEY
    end
  end
end

この例では、KEYが定義されたのはCarクラスが定義されているレキシカルスコープですが、呼び出されているのはスコープが閉じて、別のレキシカルスコープの内部からです。

したがってRubyは見つけ出せずNameErrorの例外を出します。endが来るとその内部で定義した定数のことを忘れてしまうと覚えてしまってもいいかもですね。

もしKEY定数を参照したい場合は、

  class House
    def car_key
      p User::Car::KEY
    end
  end

のように修飾を付けることで可能になります。

Effective Ruby

Effective Ruby

Ruby on Rails 5アプリケーションプログラミング

Ruby on Rails 5アプリケーションプログラミング