【Kotlin/JVM】要素に不足がない EnumMap

Java では、ある enum 型についてそのすべての列挙定数が網羅されていることをコンパイル時に保証することはできない。

Kotlin では、`when`式（や `if`-`else` 式）を使えばできる。

```Kotlin
/** 例として使用する、三原色を表すクラス。 */
enum class PrimaryColor {
    RED, GREEN, BLUE
}

val primaryColor = PrimaryColor.values().random()

val primaryColorAsJpOk = when (primaryColor) {
    RED -> "赤"
    GREEN -> "緑"
    BLUE -> "青"
} // OK。すべての列挙定数が網羅されている。

val primaryColorAsJpNg = when (primaryColor) {
    GREEN -> "緑"
    BLUE -> "青"
} // コンパイルエラー！　RED が抜けている。
```

## `Map`

enum 型の値から別の型の値へのマッピングを `Map` オブジェクトで扱いたいこともあるだろう。
次のようにすれば、列挙定数が網羅されていることが保証された `Map` を生成することができる。
（注意：後述するように、これでは効率がよくない。）

```Kotlin
// 効率がよくないため使わないこと！
val primaryColorToJpMapInefficient: Map<PrimaryColor, String> =
    PrimaryColor.values().associateWith {
        when (it) {
            RED -> "赤"
            GREEN -> "緑"
            BLUE -> "青"
        }
    }
```

## `java.util.EnumMap`

Kotlin/JVM では `Map` のキーを enum 型にする場合には `Map` インターフェイスの実装として
`java.util.EnumMap` クラスを使うと効率がよい。

```Kotlin
val primaryColorToJpMapEfficient: Map<PrimaryColor, String> =
    PrimaryColor.values().associateWithTo(
        EnumMap(PrimaryColor::class.java)
    ) {
        when (it) {
            RED -> "赤"
            GREEN -> "緑"
            BLUE -> "青"
        }
    }
```

## 関数を抽出

次のような関数を定義しておけば

```Kotlin
/**
 * enum クラスである [K] 型のすべての列挙定数がキーとして存在する
 * [EnumMap] インスタンスを生成して返す。
 * 
 * @param K [EnumMap] のキーの型。
 * @param V [EnumMap] の値の型。
 * @param transform [EnumMap] のキーから値を生成する関数。
 * @return すべての列挙定数がキーとして存在する [EnumMap]
 */
inline fun <reified K : Enum<K>, V> filledEnumMap(
    transform: (K) -> V
): EnumMap<K, V> =
    enumValues<K>().associateWithTo(EnumMap<K, V>(K::class.java), transform)
```

次のように簡潔に書くことができる。

```Kotlin
val primaryColorToJpMap: Map<PrimaryColor, String> =
    filledEnumMap {
        when (it) {
            RED -> "赤"
            GREEN -> "緑"
            BLUE -> "青"
        }
    }
```

### 警告を抑制

実は前記の書き方だと次の箇所に警告が出る。

```Kotlin
val primaryColorToJpMap: Map<PrimaryColor, String> =
    filledEnumMap {
        when (it) { // it に警告が出る
            RED -> "赤"
            GREEN -> "緑"
            BLUE -> "青"
        }
    }
```

`it` の型が `PrimaryColor` ではなく `PrimaryColor!` であり、
`when` 式に `it` が `null` だった場合の処理が書かれていないからだ。

これを抑制するには
次のように `filledEnumMap` 関数の型パラメータを明示するか、

```Kotlin
val primaryColorToJpMap: Map<PrimaryColor, String> =
    filledEnumMap<PrimaryColor, String> { // 型パラメータを明示
        when (it) {
            RED -> "赤"
            GREEN -> "緑"
            BLUE -> "青"
        }
    }
```

次のように警告を抑制するアノテーションを付ける必要がある。

```Kotlin
val primaryColorToJpMap: Map<PrimaryColor, String> =
    filledEnumMap {
        @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") // 警告を抑制するアノテーション
        when (it) {
            RED -> "赤"
            GREEN -> "緑"
            BLUE -> "青"
        }
    }
```

他によい方法をご存じであればご教授いただけると幸いです。

## おまけ：実行時にチェックする

不足している列挙定数がないことを実行時にチェックする関数・プロパティも作成した。

```Kotlin
/**
 * enum 型をキーとする [Map] 型であるレシーバーのキーに、
 * そのキーの型である enum 型の列挙定数がすべて存在するかどうか。
 *
 * @param K [EnumMap] のキーの型。
 * @param V [EnumMap] の値の型。
 */
inline val <reified K : Enum<K>, V> Map<K, V>.isFilled
    get() = size == enumValues<K>().size

/**
 * enum 型をキーとする [Map] 型である引数 [map] のキーに、
 * そのキーの型である enum 型の列挙定数のいずれかが存在しなければ
 * [IllegalArgumentException] をスローする。
 */
inline fun <reified K : Enum<K>, V> requireFilled(map: Map<K, V>) {
    require(map.isFilled) {
        (enumValues<K>().toSet() subtract map.keys)
            .joinToString()
            .let { "不足している列挙定数があります。不足している列挙定数: $it" }
    }
}

/**
 * enum 型をキーとする [Map] 型である引数 [map] のキーに、
 * そのキーの型である enum 型の列挙定数のいずれかが存在しなければ
 * [IllegalStateException] をスローする。
 */
inline fun <reified K : Enum<K>, V> checkFilled(map: Map<K, V>) {
    check(map.isFilled) {
        (enumValues<K>().toSet() subtract map.keys)
            .joinToString()
            .let { "不足している列挙定数があります。不足している列挙定数: $it" }
    }
}
```

／以上