前提
Scalaには言語組込で列挙型がありません。
列挙型のようなものが欲しい場合、公式にEnumerationという仕組みが用意されていますがこちら評判があまり良くなく、
sealed traitとobjectを使ったパターンがシンプルで良いとよく採用されています。
個人的によく使うパターンはこれです。
sealed trait DNA
object DNA {
object A extends DNA
object C extends DNA
object G extends DNA
object T extends DNA
}
以下のポイントが気に入っています。
- (基本ですが)sealedであることでDNA型のパターンマッチ時に網羅漏れがあるとコンパイラが警告してる
- 列挙されている型(A,C,G,T)がひとまとまりで扱いやすい
- DNA型って何があったっけと思ったとき、
DNA.
と打てばIDEが候補を補完してくれる - こう書くとIntellijのProject View(ファイルツリー)がこのソースコードファイルをDNA型専用のファイルだと認識してくれる
- Intellijは一つのソースコードファイルのトップレベルで複数の型を宣言すると、そのファイルがどの型について書かれているのかわからなくなりProject Viewでの表示が型からファイル名へと落ちる
問題
列挙対象の値の型としてobjectを使うかcase objectを使うか。
上記URL先ではcase objectが使われていますが、Scalaオブジェクトメモ(Hishidama's Scala object Memo)で以下のように述べられている通り明確なメリットが見えません。
せいぜいtoString()が実装される点くらいしか利点が無いような気が^^;
調査
先程のDNAの例でTだけcase objectにして-Xprint:typerコンパイルした結果が以下。
・・・
object G extends AnyRef with DNA {
def <init>(): DNA.G.type = {
G.super.<init>();
()
}
};
case object T extends AnyRef with DNA with Product with Serializable {
def <init>(): DNA.T.type = {
T.super.<init>();
()
};
override <synthetic> def productPrefix: String = "T";
<synthetic> def productArity: Int = 0;
<synthetic> def productElement(x$1: Int): Any = x$1 match {
case _ => throw new IndexOutOfBoundsException(x$1.toString())
};
override <synthetic> def productIterator: Iterator[Any] = runtime.this.ScalaRunTime.typedProductIterator[Any](T.this);
<synthetic> def canEqual(x$1: Any): Boolean = x$1.$isInstanceOf[DNA.T.type]();
override <synthetic> def hashCode(): Int = 84;
override <synthetic> def toString(): String = "T";
<synthetic> private def readResolve(): Object = DNA.this.T
・・・
単なるobjectに対してcase objectはProduct, Serializableの継承に加えてtoString, hashCodeメソッドが実装されていることがわかる。
scala - Difference between case object and object - Stack Overflow
stackoverflowでもcase objectと単なるobjectの差はSerializableとtoStringの実装だと書かれていた。
objectをserializableにしたいときはしたくないときよりかなりまれ。
またtoStringも本来は自分できちんと実装したほうがよい。
それらと継承ツリーが複雑になるデメリットを勘案すると、これならcase objectではなくobjectを使ったほうが良い気がする。
結論
疑似列挙型パターンを使うときはcase objectではなくobjectを使いましょう。
2018/09/18 追記
冗長なabstract classをtraitに、case objectをobjectへと修正 by bigwheel · Pull Request #461 · dwango/scala_text
識者に説明していただいたところ、どうやらcase objectを使う背景は次のようなものであるらしいことがわかりました。
- 代数的データ型 - Wikipediaという考え方がある
- 代数的データ型の代表的な実装としてsealed abstract classとcase classがある
- ②の特殊系としてcase objectを使っている?
また代数的データ型などを意識しない場合でも、case objectのcase
を単に列挙型の各値を表すマーカー的に捕えて使う向きもあるようです。
ですので純粋機能的にはcase objectには大きなメリットはないですが、コーダーの認識上のメリットはあります。
チーム内でそこを共有できるのならあえてcase objectを使うことも全然アリだという結論になりました。