Ruby で class や module をネストして書く場合と Foo::Bar のようにコンパクトに書く場合の挙動の違いについて
はじめに
Ruby では、階層的に class や module を定義する際に、2 種類の書き方があります。
class Foo
class Bar
end
end
class Foo::Bar
end
この 2 種類の書き方にはどのような違いがあるのかについて説明します。
なお、本稿では便宜上、前者のような書き方をネスト記法、後者のような書き方をコンパクト記法と呼ぶことにします。
TL;DR
- ネスト記法は各階層の名前空間ごとに class か module かを明確に指定するが、コンパクト記法は指定しない
- ネスト記法は未定義の場合に新たに定義するが、コンパクト記法は未定義の場合は例外が発生する
class と module を明確に区別するかどうか
ネスト記法ではそれぞれの名前空間に対して class か module かを明確に指定します。
class Foo
class Bar
end
end
上記のコードにおいて、Foo
は class であり、Bar
も class です。
一方で、コンパクト記法では、最後の(一番右端の)名前空間に対して class や module が指定されるのみで、途中の名前空間に関しては適用されません。
class Foo::Bar
end
上記のように書かれていると、一見、Foo
も Bar
も class として指定しているように見えますが、実際に class を指定しているのは Bar
のみで、Foo
は class なのか module なのかは指定していないことになります。
この違いにより、例えば以下のようなコードを書いたときに挙動が異なります。
ネスト記法の場合
module Foo
end
require './foo.rb'
class Foo
class Bar
def greet
puts 'Hello'
end
end
end
Foo::Bar.new.greet
上記のコード bar.rb
を実行すると、Foo is not a class (TypeError)
という例外が発生します。foo.rb
で Foo
が module として定義されているのに Foo
を class として定義しようとしているためです。
bar.rb
を以下のように書き換えれば正しく動作します。
require './foo.rb'
- class Foo
+ module Foo
class Bar
def greet
puts 'Hello'
end
end
end
Foo::Bar.new.greet
コンパクト記法の場合
module Foo
end
require './foo.rb'
class Foo::Bar
def greet
puts 'Hello'
end
end
Foo::Bar.new.greet
上記のコードは正しく動作します。class Foo::Bar
と書いた場合、この部分だけでは Foo
は class として参照するのか module として参照するのかはわかりません。Foo
が module として定義されていれば module になり、class として定義されていれば class になります。
上記のコードにおいては、bar.rb
の Foo
は module となりますが、foo.rb
を以下のように書き換えた場合は bar.rb
の Foo
は class として参照されます。
class Foo
end
class Foo::Bar
と書いた場合の Foo
は、class Foo
があれば class になり、module Foo
があれば module になるということですね。
未定義の場合に定義されるか例外になるか
ここまでの話を聞くと Foo::Bar
のほうがよしなに判断してくれて便利だと思いますが、実はもうひとつ違いがあります。それは、未定義の場合の挙動です。
先ほどの例において、foo.rb
がなかった場合(bar.rb
で require './foo.rb'
をしなかった場合)を考えてみましょう。
ネスト記法の場合
class Foo
class Bar
def greet
puts 'Hello'
end
end
end
Foo::Bar.new.greet
上記のコードは正しく動作します。
ネスト記法では、Foo
という class が存在しなかった場合には、新しく定義されます。つまり、Foo
という class が存在していればそれを参照し、存在していなければ定義するという挙動になります。
上記のコード例の場合だと、class Foo
で Foo
という class が定義されたことになります。
以下のように、すでに Foo
という class が foo.rb
で定義されていて、それを読み込んでいる場合は、bar.rb
の class Foo
では foo.rb
の Foo
が参照されることになります。
class Foo
end
require './foo.rb'
class Foo
class Bar
def greet
puts 'Hello'
end
end
end
Foo::Bar.new.greet
コンパクト記法の場合
class Foo::Bar
def greet
puts 'Hello'
end
end
Foo::Bar.new.greet
上記のコードを実行すると uninitialized constant Foo (NameError)
という例外が発生します。Foo
が未定義であるためです。
コンパクト記法では、最後(一番右端)以外の名前空間(上記のコード例だと Foo
)がすでにどこかで定義されていることが前提となります。Foo
という class もしくは module が定義されていれば上記のコードは正しく動きますが、今回は Foo
がどこにも定義されていないため例外が発生してしまいます。
つまり、このケースにおけるネスト記法とコンパクト記法の違いは、最後(一番右端)以外の名前空間が定義されていなかった場合に定義するかしないかの違いとなります。コンパクト記法では、定義されていなかった場合は例外が発生します。
RuboCop のルール
RuboCop ではこの 2 種類の記法を統一するためのルールがあります。RuboCop::Cop::Style::ClassAndModuleChildren
というルールです。
Class: RuboCop::Cop::Style::ClassAndModuleChildren
nested
もしくは compact
のいずれかの値を設定でき、nested
だとネスト記法、compact
だとコンパクト記法になります。デフォルトは nested
になっています。
参考: RuboCop | Style/ClassAndModuleChildren EnforcedStyle
おそらく RuboCop の方針としては、うっかり未定義の class や module を参照しようとして例外が発生して落ちるということを防ぐために nested
がデフォルト値になっているのではないかと思います。
一方で、コンパクト記法のほうは class か module かを意識する必要がなく、かつ、すっきり書けるというメリットもあります。class や module が定義されていることが確実に保証される場合においてはコンパクト記法を使うのもありかと思います。
まとめ
ネスト記法
- ネスト記法では各階層の名前空間ごとに class か module かを明確に指定する
- module として定義されているのに class を指定すると例外が発生する(逆も然り)
- ネスト記法ではその名前空間が定義されていれば参照し、定義されていなければ新たに定義する
コンパクト記法
- コンパクト記法では最後(一番右端)以外の名前空間に対しては class か module かを指定しない
- class として定義されていれば class になり、module として定義されていれば module になる
- コンパクト記法ではその名前空間が定義されていれば参照するが、定義されていなければ例外が発生する
Discussion