scala.Seq のマスキング

in

現行の Scala 2.13.0-M5 のままで行くと、scala.Seqscala.collection.Seq から scala.collection.immutable.Seq に変更される予定だ。Scala 2.13 collections rework に何故今まで不変じゃなかったのかの解説が少し書かれている。行間から推し量ると、scala.Seq がデフォルトで不変になることを喜ぶべきだと言っているんだと思う。

デフォルトで列が不変になることはアプリや新しく書かれるコードには良いことだと思う。ライブラリ作者にとってはもう少しこみいっているかもしれない。

  • あなたがクロスビルドされたライブラリを持っていて
  • ライブラリのユーザも複数の Scala バージョンを使っていて
  • ライブラリのユーザが Array(...) を使っていた場合

この不変 Seq への変更は、breaking change つまり非互換な API 変更となりうる。

失敗例としては scopt/scopt#218 がある。僕が scopt のクロスビルドを行ったが、args を渡せなくなったらしい。Scala 2.13.0-M5 においても argsArray[String] のままだ。

シンプルな修正は全てのソースにおいて scala.collection.Seq を import することだ。僕が欲しいのは Seq を使うとコンパイルが通らなくなることだ。

scala.Seq を unimport する

まず最初にやってみたのは scala.Seq を unimport して、scala.collection.Seqscala.collection.immutable.Seq のどちらかを import することを強制することだ。

import scala.{ Seq => _, _ }

最も外側にあるスコープ内でデフォルトの import scala._ によって Seq という名前が束縛されているため、これは効果が無い。あと、よく考えてみると、もし仮にそれがうまくいったとしても import 文を全てのソースに忘れずに書かなければいけないので、良い手では無い。

Jasper-M さんが -Yno-imports のことを思い出させてくれた。これは検討する価値があるかもしれない。

ダミーの Seq の定義

次に、自分のパッケージ以下に Seq という名前の trait を定義してみた:

package scopt
 
import scala.annotation.compileTimeOnly
 
/**
  * In Scala 2.13, scala.Seq moved from scala.collection.Seq to scala.collection.immutable.Seq.
  * In this code base, we'll require you to name ISeq or CSeq.
  *
  * import scala.collection.{ Seq => CSeq }
  * import scala.collection.immutable.{ Seq => ISeq }
  *
  * This Seq trait is a dummy type to prevent the use of `Seq`.
  */
@compileTimeOnly("Use ISeq or CSeq") private[scopt] trait Seq[A1, F1[A2], A3]

わざと複雑な型パラメータを使うことで既存のコードのコンパイルが通らないようになっている。例えば、コードに Seq[String] が出てきた場合は以下のようなエラーとなる:

[info] Compiling 3 Scala sources to /scopt/jvm/target/scala-2.12/classes ...
[error] /scopt/shared/src/main/scala/scopt/options.scala:434:19: wrong number of type arguments for scopt.Seq, should be 3
[error]   def parse(args: Seq[String])(implicit ev: Zero[C]): Boolean =
[error]                   ^
[error] one error found

コードが scopt パッケージ内にさえあれば、Seq の使用を防止できる。実際の Seq を使うためには以下の import を行う:

import scala.collection.{ Seq => CSeq }
import scala.collection.immutable.{ Seq => ISeq }

クロスビルド間の API semantics が統一されているべきと思うならば、public なものは全て CSeq を使うのがいいと思う。そして API が変更されるタイミングで ISeq を全面的に採用するかを検討すればいいと思う。

追記: scala.IndexedSeq

Sciss (Hanns) さんに scala.IndexSeq にも同様に影響があることを指摘してもらった。scala.Seq 対策をする場合は scala.IndexedSeq も同様に対応するべきだろう。

追記: Heiko Seq

あともう一つ Sciss (Hanns) さんに思い出させてもらったのは Heiko Seq のことだ。これは、Heiko さんが 2013年に Seq is not immutable! 書いている:

package object scopt {
  type Seq[+A] = scala.collection.immutable.Seq[A]
  val Seq = scala.collection.immutable.Seq
  type IndexedSeq[+A] = scala.collection.immutable.IndexedSeq[A]
  val IndexedSeq = scala.collection.immutable.IndexedSeq
}

これは scala.immutable.Seq を全ての Scala バージョンで採用することになる。scala.collection.Seq のままが良ければ Sciss さんのバリエーションを使えばいい:

package object scopt {
  type Seq[+A] = scala.collection.Seq[A]
  val Seq = scala.collection.Seq
  type IndexedSeq[+A] = scala.collection.IndexedSeq[A]
  val IndexedSeq = scala.collection.IndexedSeq
}

ソースを検査して CSeqISeqList と決めるのが面倒なひとはこういう手もあるかもしれない。

追記: 可変長引数 (vararg)

あと、関連する Scala 2.13 マイグレーションの事項として可変長引数 (vararg) があるというコメントを Dale 君がしていた。
Scala の言語仕様として可変長引数は scala.Seq を受け取ることになっているので、この変更によって実質 scala.collection.immutable.Seq を期待するという変更になる。あなたのユーザが API を something(xs: _*) というふうに呼び出していて、xs が配列などであった場合に影響が出てくる。これは Scala 全体の変更で、Scala 2.13 に移行するときに全員が変更しなければいけない。