Kotlinの拡張関数をわざわざ使う理由


Kotlinの拡張関数、便利ですよね。

その「便利」の意味するところは、「自身の手を入れられない箇所(ビルトインだったりサードパーティだったりのfinal class)への唯一のアプローチである」という側面が強いかと思いますが、それ以外にも便利な点があります。

それが「特定の型パラメーターのときのみ使用できる関数(メソッド)の定義」です。

toMapメソッドの例

KotlinにはtoMapメソッドがあります。List<Pair<K, V>>に対してtoMapメソッドを呼ぶと、Map<K, V>に変換してくれるやつですね1

このメソッド、List<T>(Tは非Pair<K, V>)に対して呼ぶとどうなると思いますか?答えは「呼べない」、もっと言うと「コンパイルエラーになる」です。

実際IntelliJ IDEAでは候補にすら出ません。

この挙動は定義にミソがあります。バージョン1.3.72における定義はMaps.ktに記述の通りです。シグネチャだけ抜粋したのは以下。

public fun <K, V> Iterable<Pair<K, V>>.toMap(): Map<K, V> {
    // ...
}

なるほど。この定義だと、toMapメソッドのレシーバーはただIterableならOKというわけではなく、Iterable<Pair<K, V>>である必要があります。これでList<T>では使えず、List<Pair<K, V>>なら使えるわけですね。

拡張関数のシンタックスを使うことで、あたかも「クラスの型パラメーターが特定の型のとき」という条件を指定できます。反対に、このシンタックスを使わないとこの指定は出来ません2

同一モジュールで自身の手を入れられる場合においても、わざわざ拡張関数で定義しているケースをちょいちょい見掛けて気にはなっていたのですが、これが理由のひとつかなあと思います。

ScalaのtoMapメソッド

Scalaでも同様にtoMapメソッドがあります。Kotlinと同様、Iterableの取る型パラメータがTuple2[K, V]のときのみ使用できます。

しかし、Kotlinとはアプローチが異なります。以下がtoMapメソッドの定義。

trait IterableOnceOps[+A, +CC[_], +C] extends Any { this: IterableOnce[A] =>
  // ...

  def toMap[K, V](implicit ev: A <:< (K, V)): immutable.Map[K, V] =
    // ...
}

Iterableではなく、IterableOnceOpsだったり、まあいろいろ差異はあるのですが、それは設計の差というだけなので、目を瞑りましょう。重要なのは拡張関数ではなく、普通のメソッドとして定義しているところです。

しかし、Kotlinでは見掛けない(implicit ev: A <:< (K, V))という記述があります。これが「型パラメーターAがTuple2[K, V]またはTuple2[K, V]のサブタイプでなければいけない」という制約になります。よって、Kotlinと同様にそれ以外の型パラメーターのときに呼び出すコードを書こうものなら、ちゃんとコンパイルエラーになります3

この方法、Generalized type constraintsと言います。言っていることは単純なんですが、Scalaのシンタックスと絡んだりして実現方法がちょっと複雑かな〜と思います。興味のある方はひもといてみると面白いかも。

Kotlinは拡張関数でGeneralized type constraintsを実現しているのか

ScalaにはGeneralized type constraintsなんて名前が付いているんですよね。ちょっと発音しづらいけど、なんか格好良いですよね。つい使いたくなっちゃう?ちょっとちょっと、"クセ"出てるよ〜。わかるけど〜。じゃあ明日から「Kotlinは拡張関数でGeneralized type constraintsを実現している」と言っちゃおうか。

これはちょっと疑問に思う。

Generalized type constraintsのA <:< Bは、「AがBまたはBのサブクラスのとき」という条件だが、実はA =:= Bもあり、これは「AがBのとき」という条件になる。この条件は型パラメーターAの変性とは無関係である。

しかし、Kotlinの場合、型パラメーターの変性によって左右されてしまう。toMapメソッドの場合はIterableの型パラメーターが共変であるout Tなので、あたかも「型パラメーターTがPair<K, V>またはPair<K, V>のサブクラスのとき」のように振る舞えたが、これはたまたまと言って良いと思う。

よって、「Kotlinは拡張関数でGeneralized type constraintsを実現している」は違うと思う。

まあでもあれだな!実際にこの差で困ったことはないし、気にするほどではないと思う。さっき偉そうに「Tは共変だから〜」とか言ったけど、不変でもfun <K, V, P : Pair<K, V>> Iterable<P>.toMap(): Map<K, V>で可能なはずだし4!不可能なのは=:=を実現したいのに共変のときとかかな……。

とりあえず、Generalized type constraintsではないが、「特定の型パラメーターのときのみ使用できる」というのを実現しているとは言える、が持論。

まとめ

Kotlinでわざわざ拡張関数を使うのは、「特定の型パラメーターのときのみ使用できる」というのを実現するため。ScalaのGeneralized type constraintsと同一の仕組みではないが、同じ目的をほぼ実現できている。


  1. 正確にはListではなくIterable 

  2. たぶん。他に指定方法があるなら教えてほしい……。私はちょっと分かんなかった 

  3. ちなみにIntelliJ IDEAなどで候補としては現れる。ここがKotlinとScalaの(実現方法による)違い 

  4. ただしPairはdata classなので継承不可能。なのであまり意味のない定義