🦔

Ruby で class や module をネストして書く場合と Foo::Bar のようにコンパクトに書く場合の挙動の違いについて

2021/03/02に公開

はじめに

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

上記のように書かれていると、一見、FooBar も class として指定しているように見えますが、実際に class を指定しているのは Bar のみで、Foo は class なのか module なのかは指定していないことになります。

この違いにより、例えば以下のようなコードを書いたときに挙動が異なります。

ネスト記法の場合

foo.rb
module Foo
end
bar.rb
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.rbFoo が module として定義されているのに Foo を class として定義しようとしているためです。

bar.rb を以下のように書き換えれば正しく動作します。

bar.rb
  require './foo.rb'

- class Foo
+ module Foo
    class Bar
      def greet
        puts 'Hello'
      end
    end
  end

  Foo::Bar.new.greet

コンパクト記法の場合

foo.rb
module Foo
end
bar.rb
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.rbFoo は module となりますが、foo.rb を以下のように書き換えた場合は bar.rbFoo は class として参照されます。

foo.rb
class Foo
end

class Foo::Bar と書いた場合の Foo は、class Foo があれば class になり、module Foo があれば module になるということですね。

未定義の場合に定義されるか例外になるか

ここまでの話を聞くと Foo::Bar のほうがよしなに判断してくれて便利だと思いますが、実はもうひとつ違いがあります。それは、未定義の場合の挙動です。

先ほどの例において、foo.rb がなかった場合(bar.rbrequire './foo.rb' をしなかった場合)を考えてみましょう。

ネスト記法の場合

bar.rb
class Foo
  class Bar
    def greet
      puts 'Hello'
    end
  end
end

Foo::Bar.new.greet

上記のコードは正しく動作します。

ネスト記法では、Foo という class が存在しなかった場合には、新しく定義されます。つまり、Foo という class が存在していればそれを参照し、存在していなければ定義するという挙動になります。

上記のコード例の場合だと、class FooFoo という class が定義されたことになります。

以下のように、すでに Foo という class が foo.rb で定義されていて、それを読み込んでいる場合は、bar.rbclass Foo では foo.rbFoo が参照されることになります。

foo.rb
class Foo
end
bar.rb
require './foo.rb'

class Foo
  class Bar
    def greet
      puts 'Hello'
    end
  end
end

Foo::Bar.new.greet

コンパクト記法の場合

bar.rb
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 になる
  • コンパクト記法ではその名前空間が定義されていれば参照するが、定義されていなければ例外が発生する
GitHubで編集を提案

Discussion