search term:

masking scala.Seq

As of Scala 2.13.0-M5, it’s planned that scala.Seq will change from scala.collection.Seq to scala.collection.immutable.Seq. Scala 2.13 collections rework explains a bit about why it’s been non-immutable historically. Between the lines, I think it’s saying that we should celebrate that scala.Seq will now be immutable out of the box.

Defaulting to immutable sequence would be good for apps and fresh code. The situation is a bit more complicated for library authors.

this change to immutable Seq could be a breaking change to your API.

An example of such breakage is scopt/scopt#218. I cross-published scopt, and now it won’t work with args. Even in Scala 2.13.0-M5, args is an Array[String].

A simple fix is to import scala.colletion.Seq in all source code. But I want to make it such that using Seq won’t compile the code.

unimporting scala.Seq

First thing I thought of is unimporting the name scala.Seq, so I would be forced to import either scala.collection.Seq or scala.collection.immutable.Seq.

import scala.{ Seq => _, _ }

This does not work, since the name Seq is bound by the default import scala._ in the outermost scope. Even if it did work, this would require remembering to put the import statement in all source code, so it’s not good.

Jasper-M reminded me about -Yno-imports, which might be an option to consider.

defining a dummy Seq

Next, I tried defining a trait named Seq under my package:

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]

I am using nonsensical type parameters so the existing code won’t compile. For example, Seq[String] in my code will be caught as follows:

[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

As long as the code is within scopt package, this should prevent the use of Seq. To use actual Seqs, we would import them as follows:

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

If you care about your API semantics being the same across cross builds you might opt for CSeq for anything public. And maybe when you bump your API, you can change them all to ISeq.

addendum: scala.IndexedSeq is affected too

Sciss (Hanns) pointed out that scala.IndexedSeq are affected in the same way. So if you’re doing this for scala.Seq you might as well check for scala.IndexedSeq too.

addendum: Heiko Seq

Sciss (Hanns) also reminded me about Heiko Seq, which Heiko wrote in Seq is not immutable! post back in 2013:

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
}

This will adopt the scala.immutable.Seq across all Scala versions. If you want to stay on scala.collection.Seq, you can use the Sciss variation:

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
}

If you don’t want to go through your source deciding whether to use CSeq, ISeq, or List, this might be a solution for you.

addendum: vararg

Dale reminded about related Scala 2.13 migration issue, which is vararg. Given that Scala specification specifies that scala.Seq is passed on, vararg parameters will expect scala.collection.immutable.Seq. This matters if your users are calling your API as something(xs: _*), and xs happens to be an array etc. This is a Scala wide change, and it’s something everyone has to change if you migrate to Scala 2.13.