列挙型(enum)の使い方

Scala 3.3.4
最終更新:2023年12月7日

[AD] Scalaアプリケーションの開発・保守は合同会社ミルクソフトにお任せください

この記事では、Scalaの列挙型(enum)を使う方法について解説します。

列挙型とは「種類」を漏れなく表現する方法

「種類」をもれなく表現するために使うのが列挙型です。

例えば、オペレーティングシステムを例に考えてみましょう。

世の中には数多くのオペレーティングシステムがあります。

これから作るシステムでそのうちのいくつかに対応することにしましょう。

trait OS

対応するOSは、WindowsとmacOSとLinuxとしましょう。

class Windows extends OS class MacOS extends OS class Linux extends OS

ただ、このようにクラスで表すとなるとちょっと不便です。

というのも、システムとして対応するべきオペレーションシステムは限定されているのに、プログラム上はこれ以外にもOSトレイトを継承したクラスが存在しうるからです。

ここで列挙型の出番です。

列挙型に関するScala 3系とScala 2系の違い

Scala 3で列挙型を定義するには、enumキーワードを使って以下のように記述します。

enum OS { case Windows, MacOS, Linux }

実のところ、Scala 2系にも以下のようなEnumerationクラスはありました。

object OS extends Enumeration: type OS = Value val Windows, MacOS, Linux = Value

ところが、使い勝手が悪いため、下のような定義を手作業で書くのが慣習になっていました。

sealed trait OS object OS: case object Windows extends OS case object MacOS extends OS case object Linux extends OS

Scala 3のenumキーワードは、上のような定義を自動で生成してくれるシンタックスシュガーなわけです(生成されるコードは実際には異なります。後述)。

enumキーワードにより手軽に列挙型を定義できるようになり、非常に嬉しいですね。

Scala 3で列挙型を使う

コンパニオンオブジェクトを定義する

enumキーワードの実態がsealed traitですので、以下のようにコンパニオンオブジェクトを定義することも可能です。

enum OS { case Windows, MacOS, Linux }
object OS { def sample: Unit = () }

パラメータを与えてみる

OSのバージョン違いを扱ってみましょう。

バージョン名をパラメータで持たせるには以下のように書くことができます。

sealed trait OS(version: String) object OS { enum Windows(val version: String) extends OS(version) { case Windows8 extends Windows("Windows 8") case Windows10 extends Windows("Windows 10") } enum MacOS(val version: String) extends OS(version) { case Mojave extends MacOS("macOS Mojave 10.14") case Catalina extends MacOS("macOS Catalina 10.15") } enum Linux(val version: String) extends OS(version) { case Ubuntu extends Linux("Ubuntu") case CentOS extends Linux("CentOS") } }

例には挙げていませんが、複数のパラメータを持たせることもできます。

クラス名やバージョン名は以下のようにして出力することができます。

出力結果とともに確認してみましょう。まずはクラス名です。

import OS.Windows.* println(s"className: ${Windows10}")
className: Windows10

こちらはバージョン名です。

import OS.Windows.* println(s"version: ${Windows10.version}")
version: Windows 10

番号を取得してみる

enumを使うと自動的に番号が割り振られます。

Scala 2系にはない機能ですね。

割り振られた番号はordinalメソッドで取得することができます。

import OS.Windows.* println(s"ordinal: ${Windows10.ordinal}")
ordinal: 1

ただし、これはenumキーワードの中でのみ一意です。

その外では改めて0から割り振られるので注意してください。

例えば、上の例ではCentOSの番号はWindows10の番号と一致します。

import OS.Linux.* println(s"ordinal: ${CentOS.ordinal}")
ordinal: 1

クラス名から逆引きをする

enumを使うと、valueOfメソッドが生成され、クラス名からクラスを逆引きすることができるようになります。

これもScala 2系にはない機能ですね。

println(s"OS: ${OS.Windows.valueOf("Windows10")}")
OS: Windows 10

クラス名のリストを取得する

さらに、enumを使うと、valuesメソッドが生成され、含まれるクラスをArrayとして受け取ることができるようになります。

Arrayなので一旦Listへ変換して文字列として出力してみます。

println(s"values: ${(OS.Windows.values.toList.map(_.toString))}")

以下のように出力されます。

values: List(Windows8, Windows10)

Javaとの互換性

それにしても、なぜArrayが返るのでしょう?

Scalaといえば、高機能なうえにJavaとの互換性がきわめて高いことがポイントですが、

Scala 3のenumはJavaのEnumとの互換性をとりやすくなっています。

そのためにArrayが返るようになっているというわけです。

具体的に見てみましょう。

enum OS extends java.lang.Enum[OS] { case Windows, MacOS, Linux }

java.lang.Enumを継承することでJavaのEnumとして使用できるようになります。

Enumの型パラメータにはOSを指定して、enumの名前と一致させてあげるのがポイントです。

enumの実装

enumキーワードで生成された型はscala.Enumを継承します。

これは公開メソッドとしてordinalを定義しています。

また、extendsされて生成されたenumは以下のような匿名クラスインスタンスとして展開されます。

val Windows10: Windows = new Windows("Windows 10") { def ordinal: Int = 1 override def toString: String = "Windows10" //... }

また、extendsされずに生成されたenumは、一つの実装を共有しています。

タグと名前を引数とするプライベートメソッドを使ってインスタンス化されるようになっています。

val Windows: OS = $new(0, "Windows")

Scala 2系では列挙型を手書きする

さて、上述のとおり、ScalaのEnumerationクラスはあまり用いられません。

ではどうするのかというと、sealed traitcase objectを使って定義します。

先程の例を再掲します。

sealed trait OS object OS: case object Windows extends OS case object MacOS extends OS case object Linux extends OS

Scala 3で実現されたような機能が欲しい場合は、以下のように書けばOKです(すべて手作業です)。

sealed traitのかわりにsealed abstract classを使うのがポイントです。

sealed abstract class OS(val ordinal: Int, val version: String) object OS: case object Windows extends OS(0, "Windows") case object MacOS extends OS(1, "macOS") case object Linux extends OS(2, "Linux") def values: Seq[OS] = Seq(Windows, MacOS, Linux) def valueOf(version: String): Option[OS] = values.find(_.version == version)

サイト内検索