[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 trait
とcase 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)