Edited at

Rubyの名前空間とレキシカルスコープについて簡潔に説明する。

More than 1 year has passed since last update.


概要

名前空間という概念についてと、Ruby上の注意について簡単に説明したいなぁと思います。

レキシカルスコープとかいう少しばかりマニアックな話もします。

生粋のRubyistの方には退屈かもしれませんが、初心者の方は頑張って理解してみてくださいね。


本記事は過去記事

Rubyのアクセス制御について簡潔に説明する

自分の過去記事をサクッと解決+アクセスメソッド理解

Rubyの継承階層におけるmoduleの位置関係を説明する

のように、

基礎→発展 という構成ではなく

いきなり知らない概念が登場すると思います。


まずは使わない

名前空間を知らないが為に起こりうる事例を紹介します。

(あくまで悪例なのでどんなスクリプトキディでも書かないようなひどいコードにします)

例えば、次のようなコードを書きたいとします。


  • 数値の入力を受け取って、それが偶数か奇数かを判断する処理。

  • クラスを二つ作る。


sample.rb


class Judge #偶数かどうか判定

def initialize(num)
if num % 2 == 0
puts "偶数"
end
end
end

class Judge #奇数かどうか判定

def initialize(num)
if num % 2 == 1
puts "奇数"
end
end
end

Judge.new(100) #偶数かどうか調べたい!
Judge.new(99) #奇数かどうか調べたい!


目を覆いたくなるようなコードですね。

これを書いた人がやりたかった事は、

偶数か判定するクラス・奇数か判定するクラスを作って

インスタンス生成の引数に数を渡して判定したい

みたいな。

そもそも一つのクラスでいいだろ、とか

まずクラスじゃなくていいだろっていう問題は取り敢えずおいておいて、

このコードを実行しても

Judge.new

偶数判定のクラスを指しているのか、奇数判定を指しているのかわからないわけですよね。

実際に実行してみて下さい。警告出ますし、正しく出力されません。もはや立派なError


名前空間を使う

そこで、先程のコードを書いた人に名前空間を教えることにしました。

先程のようなコードは、クラスをmoduleでラッピングすることで解決できます。


sample.rb


module Even
class Judge #偶数かどうか判定

def initialize(num)
if num % 2 == 0
puts "偶数"
end
end
end
end

module Odd

class Judge #奇数かどうか判定

def initialize(num)
if num % 2 == 1
puts "奇数"
end
end
end
end


こんな感じ。

まぁここまではたのしいRubyとかにも書いてあることで、知ってた人も多いハズ。

では問題その1。

偶数判定をするクラスのインスタンスを作成する時、どのように記述すればいいのでしょうか…?

これがわからない人が意外と多いのでは…?

これももしかしたら入門書に書いてあるのかな?もう覚えてないですね

まだやってた時から一ヶ月ぐらいしか経ってないのに

正解は、こうです。


ruby;sample.rb


module Even
class Judge #偶数かどうか判定

def initialize(num)
if num % 2 == 0
puts "偶数"
end
end
end
end

module Odd

class Judge #奇数かどうか判定

def initialize(num)
if num % 2 == 1
puts "奇数"
end
end
end
end

instance = Even::Judge.new



:: is 何

これが、クラスパスセパレータとか呼ばれるやつですね。

名前なんてのはどうでもいい。

Rubyのコード上ではメソッド呼び出しやクラス内の定数を呼び出す際に使われます。

参考書などで〇〇クラスの✗✗メソッドという時にこの表記をすることが多い。

これは簡単に言うと、

ディレクトリ操作でいう絶対パスみたいなやつです。

わかりやすい例で説明しましょう。

貴方がサッカー部に所属しているとします。

同学年の部員みんなでインターハイの打ち上げです。

ところで、サッカー部には二人の大輔君が所属しています。

フォワードとディフェンダー。

さて、どちらかの大輔くんを特定して名前を呼ぶ時、

どうすればいいでしょうか??


この時、ただ

大輔君!」と呼ぶだけでは、

最初のコードのように

どちらが呼ばれているかわからないわけです。

まぁ実際にはアイコンタクトをしたりするのですが、

自分が相手の事を見ていなくても、

FWの大輔くん!」といえば確実に特定して呼べますよね?

FWという役職の大輔くんは一人しかいないわけですから。

このように、名前の重複がある場合には、

それぞれを識別出来る何か指標があれば良いわけですよね。

これを名前空間っていいます。

実際はまるっきりそういう意味ではないですが、取り敢えず名前空間の利便性はそんな感じ。

役職のようなものでなくても、

髪の長い大輔君!とか、

英語が話せる大輔君!とか。

名前空間を好きなように定義できますね。

出来るわけわかりやすいほうがいいですね。

これはメソッド呼び出しでも同じです。


sample.rb


module Namespace
class Klass

def foo
puts "yeah,im foo of Klass's instance method in Namespace"
end
end
end

class Klass
def foo
puts "yeah,im foo of Klass's instance method"
end
end

Namespace::Klass.new.foo() #=>yeah,im foo of Klass's instance method in Namespace

Klass.new.foo() #=>yea,im foo of Klass's instance method


これで完全にスッキリしたのでは?

名前空間は、主に大規模なプロダクトや、Gem開発においては必須知識となりそうです。

是非、覚えてみてくださいね。


次に、レキシカルスコープについて

更に難しくなっていきますが、頑張ってみましょうね。

レキシカルスコープは簡単に言えば、

ソースコード上の物理的な位置です。

名前空間との関連性の話をする前に、物理的なスコープの話をしておきましょうか。


sample.rb


class Klass

def initialize
puts "hi,im initialize method"
end

def foo
Klass.new
end
end

ins = Klass.new #=>hi,im initialize method

ins.foo() #=>hi,im initialize method


このコード例でいえば、

foo()メソッド内のnewメソッドは、Klassクラスでインスタンスを生成します。

一方、インスタンスを生成して変数insに代入しているのは、クラスですね。

これはどちらもKlassクラスのインスタンスを生成していることについて相違ないのですが、

ソースコード上での実際の位置が違います。

この物理的な位置関係のスコープこそがレキシカルスコープ、というわけですね。

さて、話を名前空間に戻します。


次のコードを見てみましょう。


sample.rb


module PythonVersion
VERSION = "3.7.0"
end

class PythonVersion::Introduce
def initialize(python=VERSION)
puts "PythonはVer#{python}が発表されたよ!!"
end
end

ins = PythonVersion::Introduce.new


先程までの内容を理解していれば、一見このコードに違和感を感じないかもしれません。

先程のコード例で言えばインスタンスを外から生成しているようなニュアンスで、

モジュール外からクラスを定義しています。

クラスパスセパレータをきちんと打っていますし、名前空間的な問題は解決されていますね。

しかしこのコードを実際に動かしてみると、

なんとNameErrorを発生させます。もうわけわからん。

理由はとてもシンプルで、

Rubyが継承階層内においても、レキシカルスコープ内においても、定数を見つけられないから

なんですね。

ここで

「え?モジュールをMIX-INしたらクラスの上にそのモジュールの特異クラスが挿入されるって

言ってたじゃん!!なんでなんで!嘘つき!」

となってしまったかたは一度冷静になってください。

モジュールの特異クラス?なんぞそれって方は

僕の書いたこちらの記事で解説してます。


名前空間は、別に継承階層の話とはあまり関係がありません。

その証拠に、次のコードを見て下さい。


sample.rb

module PythonVersion

VERSION = "3.7.0"
end

class PythonVersion::Introduce

end

ins = PythonVersion::Introduce.new

p ins.class #=>PythonVersion::Introduce
p ins.class.class #=>Class


うわお。

つまり、

名前空間内に作られたクラスのインスタンスは、親クラスを調べるclassメソッドを呼び出した時

「あぁー、なんかPythonVersionって名前空間のIntroduceが親らしーよ?」

と返し、

メソッドチェーンでclass.classと呼び出せば(つまり祖父母を呼び出す)

お爺ちゃんはclassクラスだってさ

と返してくるのです。

なんと!?

IntroduceクラスPythonVersionモジュールの中にあるんだから

PythonVersionがお爺ちゃんなんじゃないのか!!?!?

これで名前空間と継承階層が関係ない事がわかりました。

つまり本当に

今回の場合

Rubyは継承階層内にもレキシカルスコープ内にも定数が無いためError吐くのである!!!!!

すっきりしましたね?



ソリューション

もうこれでもかってぐらいシンプル。

クラスパスセパレータを記述するだけ。


sample.rb


module PythonVersion
VERSION = "3.7.0"
end

class PythonVersion::Introduce
def initialize(python=PythonVersion::VERSION)
puts "PythonはVer#{python}が発表されたよ!!"
end
end

ins = PythonVersion::Introduce.new #=>PythonはVer3.7.0が発表されたよ!



名前空間とレキシカルスコープの説明、ご理解いただけたでしょうか???

難しい話でしたけど、Rubyの奥深さが楽しめたら幸いです。