Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Scala 3のimport構文のワイルドカードは_ではなくて*になっている

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)

あわせて読みたい

docs.scala-lang.org

docs.scala-lang.org

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?