【Kotlin】EnumMap を使いやすくする

## 課題

Kotlin でコレクションなどを `Map` 型に変換することはよくあるでしょう。
そのときにキーが enum 型であることもよくあるでしょう。

```Kotlin:コレクションを Map に変換する
enum class MyEnum { A, B }

val map = // val map: Map<MyEnum, String>
    listOf(MyEnum.A, MyEnum.B)
        .associateWith { myEnum ->
            myEnum.toString()
        }
```

キーが enum 型の場合、`Map` の 実装として Java 標準ライブラリーの [`EnumMap` クラス](https://docs.oracle.com/javase/jp/21/docs/api/java.base/java/util/EnumMap.html)を使用するとパフォーマンスがよくなります。

```Kotlin: コレクションを EnumMap に変換する
val map = // val map: EnumMap<MyEnum!, String!>
    listOf(MyEnum.A, MyEnum.B)
        .associateWithTo(
            EnumMap(MyEnum::class.java)
        ) { myEnum ->
            myEnum.toString()
        }
```

このとき問題が3つあります。

- `EnumMap` クラスのコンストラクター引数にキーの型の `Class` インスタンスを渡す必要がある
- 変換後の型の実型パラメーターが[プラットフォーム型](https://kotlinlang.org/docs/java-interop.html#notation-for-platform-types)になる
- 変換後の型がミュータブルである

この記事ではこれらの問題を解決します。


## `EnumMap` クラスのコンストラクター引数にキーの型の `Class` インスタンスを渡す必要がある

`EnumMap` クラスのコンストラクター引数にキーの型の `Class` インスタンス（今回の例では `MyEnum::class.java`）を渡す必要があるのが面倒です。

[インライン関数の `reified`](https://qiita.com/sdkei/items/37117742113337c3738d#reified) を使用すれば実型パラメーターの `Class` インスタンスを取得することができます。
これを用いて次のような関数を作って使うようにしましょう。

```Kotlin: EnumMap を生成する関数
/**
 * 空の新しい [EnumMap] を返す。
 */
inline fun <reified K : Enum<K>, V> enumMapOf(): EnumMap<K, V> =
    EnumMap(K::class.java)
```

```Kotlin: EnumMap を生成する関数を使用する
val map = // val map: EnumMap<MyEnum!, String!>
    listOf(MyEnum.A, MyEnum.B)
        .associateWithTo(enumMapOf()) { myEnum ->
            myEnum.toString()
        }
```

これでキーの型を明示することなく `EnumMap` インスタンスを生成できるようになりました。


## 変換後の型の実型パラメーターが[プラットフォーム型](https://kotlinlang.org/docs/java-interop.html#notation-for-platform-types)になる

プラットフォーム型は、nullable か non-null かが分からない型です。
そして、分からないにもかかわらず、null を受け付けてしまいます（つまり null 安全ではありません）。
nullable は `MyEnum`、non-null は `MyEnum?` のように表されるのに対して、
プラットフォーム型は `MyEnum!` のように表されます。

先ほどの例では変換後の型が、キーも値もプラットフォーム型である `EnumMap<MyEnum!, String!>` と推論されています。
この例ではキーにも値にも null が入ることはないので、いずれもが non-null である `EnumMap<MyEnum, String>` となってほしいのですが、そうなっていません。

```Kotlin: 再掲：EnumMap を生成する関数を使用する
val map = // val map: EnumMap<MyEnum!, String!>
    listOf(MyEnum.A, MyEnum.B)
        .associateWithTo(enumMapOf()) { myEnum ->
            myEnum.toString()
        }
```

プラットフォーム型は、Kotlin コード上で型を明示してやることで nullable 型もしくは non-null 型にすることができます。
例えば次のようにします。

```Kotlin: 型を明示することでプラットフォーム型でなくする
val map = // val map: EnumMap<MyEnum, String>
    listOf(MyEnum.A, MyEnum.B)
        .associateWithTo(enumMapOf<MyEnum, String>()) { myEnum ->
            myEnum.toString()
        }
```

しかしこれはコードを書くのが面倒です。

`EnumMap` クラスは `MutableMap` インターフェイスを実装しています。
ほとんどの場合、必要なのは `MutableMap` 型のオブジェクトであって、`EnumMap` 型である必要はありません。

そこで、実装が `EnumMap` である `MutableMap` オブジェクトを生成する関数を作ってそれを使うようにしましょう。
`MutableMap` は Kotlin で定義された型なので、型パラメータがプラットフォーム型にはなることはありません。

```Kotlin: 実装が EnumMap である MutableMap を生成する関数
/**
 * [Enum] をキーとする、空の新しい [MutableMap] を返す。
 */
inline fun <reified K : Enum<K>, V> mutableMapWithEnumKeyOf(): MutableMap<K, V> =
    EnumMap(K::class.java)
```

```Kotlin: 実装が EnumMap である MutableMap を生成する関数を使用する
val map = // val map: MutableMap<MyEnum, String>
    listOf(MyEnum.A, MyEnum.B)
        .associateWithTo(mutableMapWithEnumKeyOf()) { myEnum ->
            myEnum.toString()
        }
```

これで、型を明示することなく、プラットフォーム型でない型として推論されるようになりました。


## 変換後の型がミュータブルである

コレクションを別のコレクションに変換する関数で変換後のオブジェクトを指定できる関数（〜To 関数）一般に言えることですが、
変換後の型は、引数で指定したオブジェクトの型そのままとなるので、ミュータブルになります。

このままだと意図せず変更してしまうリスクがあるので、
できるだけ早い段階でリードオンリーの型にしておきたいです。
かといって型を明示するのは面倒です。

そこでミュータブルである `MutableMap` 型をリードオンリーである `Map` 型に変換する関数を作って使うようにしましょう。

```Kotlin: MutableMap 型を Map 型に変換する関数
/**
 * [MutableMap] を [Map] として返す。
 */
fun <K, V> MutableMap<K, V>.asMap(): Map<K, V> = this
```

```Kotlin: MutableMap 型を Map 型に変換する関数を使用する
val map = // val map: Map<MyEnum, String>
    listOf(MyEnum.A, MyEnum.B)
        .associateWithTo(mutableMapWithEnumKeyOf()) { myEnum ->
            myEnum.toString()
        }
        .asMap()
```

これで、早い段階で簡潔にリードオンリーの型にできました。


## 付録: `MutableMap` を生成する関数のオーバーロード

Kotlin 標準ライブラリーには `MutableMap` を生成する関数として [`mutableMapOf`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/mutable-map-of.html) が用意されています。
この関数には、引数なしのものだけでなく、引数が `vararg pairs: Pair<K, V>` のものもあります。

今回作った関数にも、それを用意しておきましょう。

```Kotlin: 指定の内容を含む EnumMap を生成する関数
/**
 * 第一構成要素がキーであり第二構成要素が値であるペアのリストにより内容が指定された、新しい [EnumMap] を返す。
 *
 * 複数のペアが同じキーを持つ場合、結果として得られるマップはそれらのペアの最後の値を含む。
 */
inline fun <reified K : Enum<K>, V> enumMapOf(vararg pairs: Pair<K, V>): EnumMap<K, V> =
    pairs.toMap(enumMapOf())

/**
 * 第一構成要素が [Enum] 型のキーであり第二構成要素が値であるペアのリストにより内容が指定された、新しい [MutableMap] を返す。
 *
 * 複数のペアが同じキーを持つ場合、結果として得られるマップはそれらのペアの最後の値を含む。
 */
inline fun <reified K : Enum<K>, V> mutableMapWithEnumKeyOf(vararg pairs: Pair<K, V>): MutableMap<K, V> =
    pairs.toMap(mutableMapWithEnumKeyOf())
```


## まとめ

次のような関数を作ることで、

```Kotlin: Maps.kt
/**
 * 空の新しい [EnumMap] を返す。
 */
inline fun <reified K : Enum<K>, V> enumMapOf(): EnumMap<K, V> =
    EnumMap(K::class.java)

/**
 * 第一構成要素がキーであり第二構成要素が値であるペアのリストにより内容が指定された、新しい [EnumMap] を返す。
 *
 * 複数のペアが同じキーを持つ場合、結果として得られるマップはそれらのペアの最後の値を含む。
 */
inline fun <reified K : Enum<K>, V> enumMapOf(vararg pairs: Pair<K, V>): EnumMap<K, V> =
    pairs.toMap(enumMapOf())

/**
 * [Enum] をキーとする、空の新しい [MutableMap] を返す。
 */
inline fun <reified K : Enum<K>, V> mutableMapWithEnumKeyOf(): MutableMap<K, V> =
    EnumMap(K::class.java)

/**
 * 第一構成要素が [Enum] 型のキーであり第二構成要素が値であるペアのリストにより内容が指定された、新しい [MutableMap] を返す。
 *
 * 複数のペアが同じキーを持つ場合、結果として得られるマップはそれらのペアの最後の値を含む。
 */
inline fun <reified K : Enum<K>, V> mutableMapWithEnumKeyOf(vararg pairs: Pair<K, V>): MutableMap<K, V> =
    pairs.toMap(mutableMapWithEnumKeyOf())

/**
 * [MutableMap] を [Map] として返す。
 */
fun <K, V> MutableMap<K, V>.asMap(): Map<K, V> = this
```

`EnumMap` を使っていない次のようなコードを、

```Kotlin: コレクションを Map に変換する（EnumMap 不使用）
val map = // val map: Map<MyEnum, String>
    listOf(MyEnum.A, MyEnum.B)
        .associateWith { myEnum ->
            myEnum.toString()
        }
```

次のように簡潔に、`EnumMap` を使ったパフォーマンスがよいコードに書き換えられます。

```Kotlin: コレクションを Map に変換する（EnumMap 使用）
val map = // val map: Map<MyEnum, String>
    listOf(MyEnum.A, MyEnum.B)
        .associateWithTo(mutableMapWithEnumKeyOf()) { myEnum ->
            myEnum.toString()
        }
        .asMap()
```


／以上