4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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

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

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

toMapメソッドの例

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

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

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

スクリーンショット 2020-07-24 20.03.32.png

この挙動は定義にミソがあります。バージョン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なので継承不可能。なのであまり意味のない定義

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?