##1. はじめに
Scalaで列挙型(Javaでいうenum)を定義する場合、以下のようにsealed trait
(またはabstract class
)とcase object
の組み合わせで定義するのが常套手段となっています。
// 列挙メンバーの型を定義
sealed trait Fruits
// 列挙の各メンバーを内包するコンパニオンオブジェクトを定義
object Fruits {
case object Apple extends Fruits
case object Orange extends Fruits
case object Banana extends Fruits
}
参考:
ScalaのEnumerationは使うな Scalaで列挙型を定義するには
【Scala】列挙型を使おう | Developers.IO
こうすることで==
やパターンマッチで判別できるようにはなるのですが、実務で使おうと思うとまだ手間が残ります。
例えばJavaのEnum#values
のように列挙メンバーのリストが欲しくなったときは、それぞれの列挙に自前で実装する必要があります。
sealed trait Fruits
object Fruits {
case object Apple extends Fruits
case object Orange extends Fruits
case object Banana extends Fruits
// 列挙メンバーのリストは自分で実装する必要がある
lazy val values: IndexedSeq[Fruits] =
IndexedSeq(Fruits.Apple, Fruits.Orange, Fruits.Banana)
}
確かにこれでも動きはします。
しかしこの書き方の場合、要素の抜け漏れや記述順序ににも気を配らないといけません。
また列挙メンバーが増減する度に、リストの中身も合わせて修正する必要も出てきます。
更に、ここで定義したvalues
はFruits
というobject固有のメソッドとなるため、これを使って列挙全体で使えるような汎用的な処理を記述するのも難しくなります。
##2. valuesの実装(enumeratum使用)
enumeratumは、上記のようにsealed trait(またはabstract class)とcase objectで定義した列挙型を、更に扱いやすくしてくれるライブラリです。
enumeratumをインストールするためには、build.sbtに以下の記述を追加します。
※執筆時点(2020年11月)の最新バージョンは1.6.1です。
lazy val enumeratumVersion = "1.6.1"
libraryDependencies ++= Seq(
"com.beachape" %% "enumeratum" % enumeratumVersion
)
enumeratumを使うと、例えば先の列挙型は以下のようになります。
import enumeratum.{Enum, EnumEntry}
// ①列挙のメンバーとなる型のtraitにはEnumEntryを継承させる
sealed trait Fruits extends EnumEntry
// ②列挙メンバーを内包するobjectにはEnum[【列挙のメンバーの型】]を継承させる
object Fruits extends Enum[Fruits] {
case object Apple extends Fruits
case object Orange extends Fruits
case object Banana extends Fruits
// ③valuesを定義する ※どんな列挙型でもfindValuesと書くだけでいい
lazy val values: IndexedSeq[Fruits] = findValues
}
列挙のメンバーとなる型にはenumeratum.EnumEntry
を、列挙のメンバーを内包するコンパニオンオブジェクトにはenumeratum.Enum[A]
を継承させるだけです。
Enum[A]
の型引数には列挙のメンバーとなる型(ここではFruits
トレイト)を指定します。
enumeratum.Enum
トレイトは、抽象メソッドとしてvalues
メソッドを定義しています。
values
メソッドは、自身の列挙メンバーを上から順に格納したIndexedSeqを返すことが想定されています。
def values: IndexedSeq[A]
そのためvalues
を各列挙型に個別に実装しなければいけないところまでは、enumeratumを使わないケースと同じです。
特筆すべき点は、enumeratum.Enum
には列挙したメンバーのIndexedSeqを自動生成してくれるfindValues
というメソッドが用意されていることです。
protected def findValues: IndexedSeq[A] = macro EnumMacros.findValuesImpl[A]
findValues
はマクロを使い、既に定義されている列挙メンバーを自動で読み取り、上から列挙した順にIndexedSeqに詰めて返してくれます。
なので下記のようにvalues
の実装部分にfindValues
と書くだけで、わざわざ列挙メンバーを一つずつ記述せずとも、望みのリストを取得できます。
lazy val values: IndexedSeq[Fruits] = findValues
マクロの都合上、(enumeratum.Enum
を継承する)全ての列挙で個別にこの一文を記述する必要がありますが、順序や記述漏れ、メンバーの増減を気にしながら毎回手作業でIndexedSeqを実装するのと比べれば、かかる手間は雲泥の差ではないでしょうか。
また、values
メソッド自体も定義した列挙固有のメソッドではなくenumeratum.Enum
のメソッドになったので、それを利用した汎用メソッドなども定義できるようになります。
なお本来のenumeratum.Enum#values
はdef
で定義されていますが、実装時は上記のようにval
に変更することをおすすめします。1
def
のまま実装してしまうと、values
の呼び出しの度に毎回findValues
が実行されてしまうからです。
通常、列挙の中身はアプリケーションの開始から終了まで変わらないので、findValues
を一度呼び出した時点で変数にキャッシュした方が効率的です。
またこれは個人的なこだわり程度の話ですが、全ての列挙でvalues
を使うわけでもないので、自分が使うときはlazy
キーワードをつけて遅延初期化させています。
上記のようにenumeratumを使って実装した列挙に対してvalues
を呼び出すと、以下のように列挙したメンバーが列挙順に格納されたIndexedSeq(※実体はVecor)が得られることがわかります。
scala> Fruits.values
val res0: IndexedSeq[Fruits] = Vector(Apple, Orange, Banana)
これで、JavaのEnum#values
相当の動きをScalaの列挙でも実現できるようになりました。
##3. 列挙メンバーの名前を取得する/名前から列挙メンバーを取得する
enumeratum.Enum
を継承した列挙はまた、JavaでいうEnum#name
およびEnum#valueOf
相当のメソッドを使うこともできるようになります。
enumeratumにおいては、JavaのEnum#name
はenumeratum.EnumEntry#entryName
が、Enum#valueOf
はenumeratum.Enum#withName
がそれぞれ対応します。
使用例は下記のとおりです。
// entryNameで列挙メンバー名の文字列を取得できる。
scala> Fruits.Orange.entryName
val res0: String = Orange
// withNameで列挙メンバー名の文字列から該当するobjectを取得できる。
scala> Fruits.withName("Apple")
val res1: Fruits = Apple
// withNameに列挙に存在しない文字列を渡すとNoSuchElementExceptionになる。
scala> Fruits.withName("Melon")
java.util.NoSuchElementException: Melon is not a member of Enum (Apple, Orange, Banana)
at enumeratum.Enum.$anonfun$withName$1(Enum.scala:81)
(中略)
// withNameOptionは列挙メンバー名の文字列から該当するobjectをSomeに詰めて返す。
scala> Fruits.withNameOption("Banana")
val res3: Option[Fruits] = Some(Banana)
// withNameOptionに列挙に存在しない文字列を渡すとNoneを返す。
scala> Fruits.withNameOption("Melon")
val res4: Option[Fruits] = None
この他にも、大文字小文字を考慮したり、スネークケースやキャメルケースを考慮する機能も用意されています。
詳しくはenumeratumのREADMEを参照してください。
##3. 列挙に個別の値を設定する
列挙の重要な役割として、いわゆる「区分値」のようなものを持たせ、数値定数などの代わりに型安全に管理するという使い方があります。
これは下記のように、sealed abstract class
とcase object
を組み合わせることで実現できます。
/** 発送状況enum
* @param value 発送状況区分値
*/
sealed abstract class DeliveryStatus(val value: Int)
object DeliveryStatus {
case object OrderConfirmed extends DeliveryStatus(1) // 注文済み
case object Shipped extends DeliveryStatus(2) // 発送中
case object Delivered extends DeliveryStatus(3) // 発送済
case object Failed extends DeliveryStatus(9) // 発送失敗
}
こうすることで、プログラム内では列挙型で演算しつつ、画面やDBなどへデータを渡すときには内部の区分値を保存することができるようになります。
ですがこの書き方の場合、「列挙 → 区分値」はいいのですが、「区分値 → 列挙」への変換が難しくなります
// 列挙 → 区分
def deliveryStatusToValue(deliveryStatus: DeliveryStatus): Int = deliveryStatus.value // 内部の値を取り出すだけでいい
// 区分値 → 列挙
def valueToDeliveryStatus(value: Int): DeliveryStatus = ??? // どうする?
これはenumeratumを使うなどして、列挙のSeqを取得できれば実装自体は可能です。
sealed abstract class DeliveryStatus(val value: Int) extends EnumEntry
object DeliveryStatus extends Enum[DeliveryStatus] {
case object OrderConfirmed extends DeliveryStatus(1) // 注文済み
case object Shipped extends DeliveryStatus(2) // 発送中
case object Delivered extends DeliveryStatus(3) // 発送済
case object Failed extends DeliveryStatus(9) // 発送失敗
lazy val values: IndexedSeq[DeliveryStatus] = findValues
// valuesがあれば、区分値からの逆引きMapを作れる
lazy val valueToDeliveryStatusMap: Map[Int, DeliveryStatus] =
values.map(v => v.value -> v).toMap
// 区分値からの逆引きMapを使って、区分値から列挙を取得する
def withValue(value: Int): DeliveryStatus =
withValueOption(value).get
def withValueOption(value: Int): Option[DeliveryStatus] =
valueToDeliveryStatusMap.get(value)
}
// withValueメソッドを使うことで区分値から列挙への変換が可能になる
scala> DeliveryStatus.withValue(9)
val res0: DeliveryStatus = Failed
どうでしょうか?
確かに実装はできたものの、区分値を使う全ての列挙型でこのコードをコピペしないといけないと考えると、少々気が滅入るのではないでしょうか。
この問題も、enumeratumを使うことで解決できます。
これまではenumeratum.Enum[A]
を使っていました。
列挙に値を持たせたい場合は、enumeratum.values
パッケージ内のValueEnum[ValueType, A]
およびValueEnumEntry[ValueType]
を使用します。
sealed trait ValueEnum[ValueType, EntryType <: ValueEnumEntry[ValueType]] {
def values: IndexedSeq[EntryType]
sealed trait ValueEnumEntry[ValueType] {
def value: ValueType
}
ValueEnum
はEnum
と同様values
メソッドを定義しています。
また値を持つため、値の型引数(ValueType
)も追加されています。
ValueEnumEntry
はvalue
という抽象メソッドを定義しているため、このtraitを継承した列挙メンバーはvalue
を実装する必要があります。
なおValueEnum
およびValueEnumEntry
自体はsealed
で宣言されているので、実際に使うときはその派生クラスとして宣言されている各種の型に応じた抽象型を使うことになります。
執筆時点(2020年11月)では、値に使える型としてInt
, Long
, Short
, String
, Byte
, Char
の6種類がサポートされています。
// ValueEnum.scalaで定義されている各種trait
trait IntEnum[A <: IntEnumEntry] extends ValueEnum[Int, A]
trait LongEnum[A <: LongEnumEntry] extends ValueEnum[Long, A]
trait ShortEnum[A <: ShortEnumEntry] extends ValueEnum[Short, A]
trait StringEnum[A <: StringEnumEntry] extends ValueEnum[String, A]
trait ByteEnum[A <: ByteEnumEntry] extends ValueEnum[Byte, A]
trait CharEnum[A <: CharEnumEntry] extends ValueEnum[Char, A]
// ValueEnumEntry.scalaで定義されている各種抽象クラス
abstract class IntEnumEntry extends ValueEnumEntry[Int]
abstract class LongEnumEntry extends ValueEnumEntry[Long]
abstract class ShortEnumEntry extends ValueEnumEntry[Short]
abstract class StringEnumEntry extends ValueEnumEntry[String]
abstract class ByteEnumEntry extends ValueEnumEntry[Byte]
abstract class CharEnumEntry extends ValueEnumEntry[Char]
このValueEnum
を使ってDeliveryStatus
を書き換えると、以下のようになります。
DeliveryStatus
では区分値にInt
を使っていたので、ここではIntEnum
およびIntEnumEntry
を使うこととします。
import enumeratum.values.{IntEnum, IntEnumEntry}
// ①継承元をIntEnumEntryに変更 ※valueを定義する必要あり
sealed abstract class DeliveryStatus(val value: Int) extends IntEnumEntry
// ②継承元をIntEnumに変更
object DeliveryStatus extends IntEnum[DeliveryStatus] {
case object OrderConfirmed extends DeliveryStatus(1) // 注文済み
case object Shipped extends DeliveryStatus(2) // 発送中
case object Delivered extends DeliveryStatus(3) // 発送済
case object Failed extends DeliveryStatus(9) // 発送失敗
// ③valuesはEnumのときと同じくfindValuesと書くだけ
lazy val values: IndexedSeq[DeliveryStatus] = findValues
}
見た目はenumeratum.Enum
を使ったときとほとんど変わりません。
ValueEnum
の各種派生クラスはfindValues
を実装しているため、values
の記述も同じです。
しかしValueEnum
およびValueEnumEntry
を継承することで、先に書いたような区分値からの逆引きをこのままで使えるようになります。
// withValueで区分値から該当するobjectを取得できる。
scala> DeliveryStatus.withValue(9)
val res0: DeliveryStatus = Failed
// withValueに列挙に存在しない区分値を渡すとNoSuchElementExceptionになる。
scala> DeliveryStatus.withValue(10)
java.util.NoSuchElementException: 10 is not a member of ValueEnum (1, 2, 3, 9)
at enumeratum.values.ValueEnum.$anonfun$withValue$1(ValueEnum.scala:58)
(中略)
// withValueOptは区分値から該当するobjectをSomeに詰めて返す。
scala> DeliveryStatus.withValueOpt(2)
val res2: Option[DeliveryStatus] = Some(Shipped)
// withValueOptに列挙に存在しない区分値を渡すとNoneを返す。
scala> DeliveryStatus.withValueOpt(10)
val res3: Option[DeliveryStatus] = None
ボイラープレートがぐっと減り、Scalaでも値付きの列挙を定義しやすくなりましたね!
##4. enumeratumを使う際の注意点
Scalaで列挙を定義する際の心強い味方となってくれるenumeratumですが、注意点として、enumeratum.Enum
とenumeratum.values.ValueEnum
間には一切の継承関係が存在しません。
そのためentryName
やwithName
メソッドなどはenumeratum.Enum
を継承した列挙でしか使えませんし、values
も単なる同名・同シグニチャのメソッドがEnum
とValueEnum
の両方に別々に定義されているだけです。
なので、Enum
とValueEnum
のどちらでも使える汎用的な処理を書きたいときに少し困ることがあるかもしれません。
この辺りの対処にも手を伸ばしたいところではありますが、当記事はここで一旦筆を置かせていただきます。
##5. おわりに
Scalaで列挙型を定義する際にsealed trait
とcase object
を組み合わせるのはよく知られた手法ですが、enumeratumを使用することで、更に実用的な列挙を簡単に実装できるようになります。
Javaの高機能なenumに慣れ親しんでいる方や、プログラム内になるべくボイラープレートを書きたくない方は、使用を検討してみることをおすすめいたします。
最後までお読み頂きありがとうございました。
質問や不備についてはコメント欄かTwitterまでお願いします。