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と同一の仕組みではないが、同じ目的をほぼ実現できている。