kotlinのEnumに拡張関数を定義する

kotlinのenum classはJavaのenumと同様にvalueOf(String)で当該のEnumを取得できる関数があるのですが、とある事情からこれをStringではなくenumのordinal(Int)から取得したいという要求がありました。

やりたいこと

valueOf(String)Int版が欲しい。

愚直な解決策

これを満たすために、enum class毎に以下のようなメソッドを書いて対応していました。

enum class MyEnum {
  FIRST,
  LAST;

  companion object {
    fun valueOf(ordinal: Int) = values().firstOrNull { it.ordinal == ordinal }
  }
}

これで、一応目的は達成できるのですが、ちょっとした問題があります。

  • 定義するenum classの数だけ書かなきゃいけない。

これは最悪ですね…
enum class の数が増えるたびにコピペコードが増える未来が見えます… 大文字小文字を無視して取得できるメソッドも追加して欲しいなんて言われた日には、いろんな箇所に手を入れなくてはいけなくなります…

ジェネリクスを活用する

同じようなコードを何度も書かなくてはいけない問題を解決するのがジェネリクスですが、普通に書くと以下のような形になります。

inline fun <reified E : Enum<E>> valueOf(ordinal: Int) =
    enumValues<E>().firstOrNull { it.ordinal == ordinal }

これで、enum class毎に似たようなコードを書かなくて済むようにはなるのですが、これだと最初の愚直な方法では受けられていた、IDEなどのコード補完の恩恵を受けづらいデメリットがあります。
(クラス名.って書いたらコード補完で候補出して欲しい…)

enum class E {
  FIRST,
  LAST
}

val e = E.valueOf(1) // こんな感じに書きたい。

拡張関数も併用する

kotlinには便利な拡張関数という機能があります。
これを使えば優秀なIDEであれば、まるでもともとあったメソッドかのようにコード補完で表示してくれる様になります。
これは使わない手はありませんね。
というわけで拡張関数も併用したのが次のコードです。

inline fun <reified E : Enum<E>> E.valueOf(ordinal: Int) =
    enumValues<E>().firstOrNull { it.ordinal == ordinal }

しかしこれはこれで問題があります。
この書き方だと、クラスに対して直接メソッドが生える訳ではなくクラスのインスタンスにメソッドが生えるのでクラス名.関数で呼び出しができなくなってしまいます…

E.FIRST.valueOf(1) // みたいな呼び出しになって気持ち悪いし、意味もわかりづらい...

companion objectに対して拡張する

クラス名.で関数を呼び出せるようにするにはcompanion objectに対して拡張関数を定義してあげれば良い訳です。

という訳でそれを踏まえて書くとこんな感じ。

inline fun <reified E : Enum<E>> E.Companion.valueOf(ordinal: Int) =
    enumValues<E>().firstOrNull { it.ordinal == ordinal }

にしたくなるのですが、これはダメな様で(型パラメータE がcompanion objectを持つ保証がないから?)コンパイルできません。

ならばということで、

inline fun <reified E : Enum<E>> Enum.Companion.valueOf(ordinal: Int) =
    enumValues<E>().firstOrNull { it.ordinal == ordinal }

これは行けるかと、いざ実際に使ってみようと思うと認識されていないようで

E.valueOf(1)

と書いても使えません。 (Enum.valueOf(Int)に生えてしまいました。 Enum自体はインターフェースという訳ではないのでそうなる様です。)

普通のクラス、インターフェースのcompanion objectに対してはすんなり拡張させてくれたのにEnumはそうはいかないようです…

最終的な解決策(インターフェースも併用する)

という訳でどうしようか悩んだ挙句、companion onjectにインターフェースを適用することで無理やり実現しました。

まずはEnumを継承したインターフェースを作成します。(拡張関数の受け皿としてのインターフェースなので中身は特に定義しません。)

interface EnumExtension<E : Enum<E>>

で、それのインターフェースに対して拡張関数を定義します。

inline fun <reified E : Enum<E>> EnumExtension<E>.valueOf(ordinal: Int) =
    enumValues<E>().firstOrNull { it.ordinal == ordinal }

最後に、このインターフェースをそれぞれのenum classの companion objectに継承させます。

enum class E {
  FIRST,
  LAST;

    companion object : EnumExtension<E>
}

こうすることで無事、同じようなコードを書くことを極力抑えつつ、IDEなどのコード補完の恩恵も受けられるようになりました。

努力の結晶を置いときます。
https://github.com/ChanTsune/kotlin-enum-extensions

最後に

やっぱり強引な解決法(毎回companion object : EnumExtension<E>書くのめんどくさい)なので、もっと綺麗に拡張関数を定義できる方法があれば知りたいです。
知っている方がいれば教えてください。