Scala 3には大きな構文上の変更、例えばimplicitまわりが整理されてgivenなどに変化したりしている一方で、細やかな機微にわたる変更も含まれている。今回説明するimport構文上のワイルドカード記号の変更もその一部。
Scala 2では、あるパッケージに属するものを全てインポートしてくるには以下のように_
をワイルドカードとして用いていた:
import java.util._
一方、Scala 3では*
をワイルドカードとして使う:
import java.util.*
ありがたいことに旧来の_
も 互換性がサポートされているようで使うことができるが、基本的には*
を使うことが推奨される。
なぜ_
ではなく*
なのか
Scala 3でワイルドカード記号が変更された背景には、_
のセマンティックの混乱がある。Scalaでは従来より、パッケージの一部をimportしないという機能(いわゆるshadowing)が用意されていた:
// Scala 2 import java.util.{ Random => _, _ }
上掲の例ではRandom
はimportされず、それ以外の全てのシンボルをimportする。勘の良い読者ならばお気付きだろうが、ここでセマンティックの競合が起こってしまっている。
Random
のimportを防ぐ、つまり匿名に写しますよ、という意味での_
java.util
に所属する全てのシンボルという意味での_
このように、_
には匿名と全てという別々の意味が与えられていた。Scala 3以降はこの曖昧さを排除し、別々の記号を与えることでセマンティックを以下のように明確化した。
- 匿名という意味で
_
を用いる - 全てのという意味で
*
を用いる
これにより、Scala 3ではshadowingを以下のように表現できるようになった:
// Scala 3 import java.util.{ Random as _, * } // =>ではなくasを使うようになった
Random
はimportせずに、その他の全てのシンボルをimportするというセマンティックがより一層明確になっていることがわかる。
補足
偉そうなことを書いているが、根拠となる情報がない。DottyのIssueを探しても見当らないので、実は全然違うモチベーションで変更されたのかもしれないが、他にモチベーションが見当らないので、セマンティック競合を防ぐためにこうなった、という説明をしている。
おまけ: 型におけるワイルドカード記号の変更
ところで、このようなセマンティック明確化のための記号の修正は型パラメータまわりでも行われている。
Scala 2では、F[_]
と書いたとき、文脈によって2つのセマンティックを使い分ける必要があった:
- あらゆる型をパラメータとして受け取る
F
という意味 - なんでも良いがパラメータを受け取る型
F
があるという意味
Scala 3では、型に対するワイルドカードとして?
記号が新たに導入される。「全ての」という意味のワイルドカードはこの記号が担うようになる。
val lis: List[?] = List(1, 2, 3) // 中身はなんでもいいList // lis.map(_ + 1) // Intの情報は捨て去られるのでコンパイルしない def twice[A <: Function0[?]](f: A): Unit = { f(); f() } // 返り値はなんでもいいFunction0であるようなAを二度呼ぶ twice(() => println("hello!")) // hello! と二度表示される
プレースホルダとしての、こういう型があるとき・・・という時に使う記号としては_
が引き続き使われる。「任意の」という意味はこの記号が担うようになる。
import cats.implicits.* import cats.* def left[A, F[_]: Applicative](x: F[A], y: F[A]): F[A] = x <* y println(left(42.some, 84.some)) // => Some(42)
F[_]、Scala 3.2 までは利用時と宣言時で全称と存在がひっくり返っているのがしんどいですよね (利用時は "∃X. F[X]" なのに宣言時は "∀X. F[X]")
— 集合論・圏論 (@Kory__3) 2023年2月22日
Scala 3.2 以降は _ は type placeholder の意味に統一されて wildcard の意味は ? に分離されるはず
— 集合論・圏論 (@Kory__3) 2023年2月22日