【Scala】列挙型を使おう

【Scala】列挙型を使おう

Clock Icon2015.11.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

列挙型(Java の enum)を使いたいなあ。あれ、でも enum キーワードが無いみたいだぞ。

Scala を使いはじめた Java プログラマにとってはあるあるだとおもいます。

Scala で列挙型を表現する方法には、大きく分けて シールドクラスEnumeration の二種類があります。字面だけを見ると Enumeration に飛びつきたくなりますが、一般的な用途には シールドクラス が多く使われます。今回はこのシールドクラスを使って列挙型を表現する方法を探っていきましょう。

目次

シールドクラスを使ってみよう

シールドクラス は、次のように定義します。

sealed abstract class Idol

sealed 修飾子がつけられたクラスは、同一ファイル内に定義されたクラスからしか継承できません。

sealed abstract class Idol

final class Haruka extends Idol
final class Chihaya extends Idol
final class Hibiki extends Idol

なので、このように宣言すると Idol クラスのサブクラスが Haruka, Chihaya, Hibiki の三種類のみであることが保証されます。あ、 Haruka クラスに final 修飾子がついているのは、 Haruka クラスのサブクラスが作られないようにするためですね(もしこれが可能だと、別ファイルに定義された class Kakka extends HarukaIdol のサブクラスになってしまいます)。

sealed と final によって Idol のサブクラスは限定されました。いま Idol 型は Haruka 型・ Chihaya 型・ Hibiki 型のいずれかを表します。

val idol: Idol = new Haruka()
val anotherIdol: Idol = new Hibiki()

ところで、sealed abstract class Idol は抽象クラスである必要があるでしょうか? Idol はコンストラクタを持っていません。ならばこれはトレイトでも問題ありませんね。

あと、 Haruka もコンストラクタや値を持っていません。おまけに final なクラスなので、拡張されることはありません。これは、いまここで 100回の new Haruka() を行った場合には、インスタンスID以外に評価可能な違いのない Haruka のインスタンスが100個ほど生成されるということです。これでは、 Haruka がクラスである必要が感じられません。 Haruka はオブジェクトが適当でしょう。

sealed trait Idol

case object Haruka extends Idol
case object Chihaya extends Idol
case object Hibiki extends Idol

final もなくなりました。 object を継承することはできませんものね。
ついでに、ケースクラスでお馴染みの case 修飾子によって toString などのボイラープレートコードを省略しています。

これで綺麗な形にまとまりました。いま Idol 型は、 Haruka, Chihaya, Hibiki いずれかの オブジェクト を表しています。

val idol: Idol = Haruka
val anotherIdol: Idol = Chihaya

idol == Haruka // -> ture
idol == anotherIdol // -> false

そしていま、私たちは Idol 型の変数に対してマッチ式を使うことができます。

val idol: Idol = Haruka

val greeting = idol match {
  case Haruka => "アイサツですよ!アイサツ!"
  case Chihaya => "挨拶、ですか?"
  case Hibiki => "アイサツだぞ!"
}

println(greeting) // -> "アイサツですよ!アイサツ"

おや。ところでこの Idol 型は単純な列挙型として申し分なさそうですね。

sealed / case object による列挙型

先ほどの例の Idol 型は列挙の体をなしていました。これを列挙型の作り方として一般化すると次の様になります。

sealed trait SampleEnum

case object A extends SampleEnum
case object B extends SampleEnum
case object C extends SampleEnum
case object D extends SampleEnum

ところで、このように sealed / case object を利用して作られた列挙型には次に示す大きな利点があります。

マッチする喜び

前述の例に上げた SampleEnum に対してマッチ式を書いてみます。

val s: SampleEnum = B

val message = s match {
  case A => "It's A."
  case B => "It's B."
  case C => "It's C."
  case D => "It's D."
}

println(message) //=> It's B.

このマッチ式は見るからに網羅的です。さて次に、このマッチ式を少し改変して次のようにしてみます。

val s: SampleEnum = B

val message = s match {
  case A => "It's A."
  case C => "It's C."
} 
//=> MatchError が発生!

この例では B に該当するケースがありません。Java でいう所のデフォルト節(case _ =>)もないので、値が B だった場合には実行時例外(MatchError)が発生してしまします。

多くの場合、これは意図しないコードによる意図しない挙動だと思います。書き手としては このマッチ式が網羅的でないことを知りたいはず です。

Scala コンパイラは、このコードを受け取ると次のワーニングを出してくれます。

warning: match may not be exhaustive.
         It would fail on the following inputs: B, D

このワーニングには、match 式が網羅的でないこと、そして欠落しているケースが B, D に対するものであることが示されています。
このようにコンパイラが教えてくれるので、私たちは安心して列挙型を扱ったコードを書くことができます(ワーニングを無視する、というファンキーなビルド戦略でない限りは!)。

値を持った列挙型

先ほどの IdolSampleEnum の例で、単純な列挙型の定義方法を知りました。
今度は値を持った列挙型を作る方法を考えてみましょう。 sealed / case を用いた列挙型の定義はとても柔軟なので、値を持った列挙型にはパターンが複数存在します。

まずは序数 (Index) の情報を持った列挙型を作ってみます。

sealed abstract class IndexedEnum(val index: Int)

case object IA extends IndexedEnum(1)
case object IB extends IndexedEnum(2)
case object IC extends IndexedEnum(3)
case object ID extends IndexedEnum(4)


val ie: IndexedEnum = IB
require(ie.index == 2)

これは想像し易いですね。 sealed trait ではなく、 sealed abstract class を使いコンストラクタ引数に値を渡していきます。もっとも代表的なパターンの一つです。
このパターンが適しているのは、上記のように 全ての要素が同じデータ構造を持つ ケースです。

では同じデータ構造を持たないケースについても確認してみましょう。スマホを表す型を定義してみます。

sealed trait SmartPhone

case object IPhone extends SmartPhone
final case class Android(vendor: String) extends SmartPhone
final case class Other(os: String, vendor: String) extends SmartPhone

case object IPhonecase object iPhone にしたい欲求に駆られますが、それは絶対にしてはいけません。これは Scala のパターンマッチに「先頭が小文字の場合は変数として束縛する」という規約があるためです。case object iPhone ではマッチ式が書けません(裏技を使えば書けますが……)。

スマホが Android の場合、ベンダ名を設定できます。iPhone のベンダは Apple に限定されますから、この情報は不要です。これら以外のスマホを表す Other には OS の名前とベンダ名の2つが設定可能になっています。

個別に値を持たせる場合、値を持つクラスは final case class として宣言します。 Idol 型を作った時のことを思い出すと、最初は final class Haruka などとして値型を宣言していました。しかし、その値型がなにも値を持たなかったために case object Haruka に切り替えたのでした。
今回は値型が実際に値を持つのですから、 final class を利用するのがよいでしょう。また case object の時と同様に、多くのボイラープレートコードを削減するために case 修飾子を使っています。

実際に上記の SmartPhone 型をマッチ式で使ってみましょう。

val s: SmartPhone = Android("ASUS")

val message = s match {
  case IPhone => "iPhone by Apple"
  case Android(vendor) => s"Android by $vendor"
  case Other(os, vendor) => s"$os by $vendor"
}

println(message) //=> Android by ASUS

Android, Other はケースクラスなので、 unapply による抽出が利用できます。そのため、 SmartPhone 型のマッチ式は上記のようにシンプルに書くことができます。

たったいま例に上げた SmartPhone 型は、もはや列挙型とは呼ばれません。一般的に、代数的データ型と呼ばれることが多いようです。代数的データ型には Scala API に用意されている Option, Either, 連結リストなども含まれています。しかしながら、袋小路に入らないためにも、いまは “値を持つことができる列挙型の亜種” 程度に捉えておくのがよいと思います。

まとめ

今回は Scala における列挙型の定義方法のうち、シールドクラスを用いた方法を紹介しました。 sealed / case object / final case class を利用して作った列挙型は強力な match 式のサポートを得ることができ、かつ自由に値を持たせることが可能です。

便利な Scala の列挙型と match 式を使いこなしていきましょう。ではまた!

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.