TL; DR: Prefer
import scala.collection.{immutable,mutable}
Over the eons, I developed a habit of using the following import
import scala.collection.*
to set up my use of Scala collections.
Then I can refer to collections in a very clear longhand. For example:
val alphamap = immutable.Map( "A" -> "Apple", "B" -> "Baby", "C" -> "Candy" )
or, much less frequently:
val scratchpad = mutable.Map( "Title" -> "Untitled" )
I like referring explicitly to collections as mutable.Thing
or immutable.Thing
. Yes it's more typing, but it's very clear.
But recently the practice has caused some hassles, and I've realized it's not great.
Consider this little scala-cli application:
//> using scala 3.3.1
trait Hat:
def tickets : Set[String]
object HatApp:
import scala.collection.*
import scala.util.Random
val hat = new Hat:
def tickets : Set[String] = Set("Alice", "Bob", "Carol")
@main
def winner = println( Random.shuffle(hat.tickets.toList).head )
Try running it, and oops!
Compiling project (Scala 3.3.1, JVM)
[error] ./unqualified-collections.scala:11:9
[error] error overriding method tickets in trait Hat of type => Set[String];
[error] method tickets of type => collection.Set[String] has incompatible type
[error] def tickets : Set[String] = Set("Alice", "Bob", "Carol")
[error] ^
Error compiling project (Scala 3.3.1, JVM)
The issue is that Set
is defined in four places:
- unqualified in
scala.Predef
- as
scala.collection.Set
- as
scala.collection.immutable.Set
- as
scala.collection.mutable.Set
.
The convenient, simple, unqualified Set
is, very sensibly, just a type alias for scala.collection.immutable.Set
.
However, after I've imported scala.collection.*
, unqualified Set
now refers to scala.collection.Set
, the base trait for both mutable and immutable sets, rather than the immutable trait it originally referred to.
A scala.collection.Set
is not a subtype of scala.collection.immutable.Set
or, equivalently, the predef's unqualified Set
.
So even though my definition of a Hat
looks trivially conformant, it is not.
It's easy to fix this just by using the longhand syntax I prefer
val hat = new Hat:
def tickets : immutable.Set[String] = immutable.Set("Alice", "Bob", "Carol")
But it's a bit surprising that a contract that was defined in terms of unqualified Set
has to be implemented in terms of immutable.Set
. If a trait was defined in terms of unqualified Set
, it's more straightforward to just implement it in terms of that.
So lately I've taken to changing how I make collections available for myself. I still prefer to be able to refer to them in explicit, clear, longhand. But instead of
import scala.collection.*
I now prefer
import scala.collection.{immutable,mutable}
This way, I can refer to collections explicitly as immutable.Whatever
or mutable.Whatever
, but when I refer to unqualified collection names, I haven't shadowed the predef definitions with rarely desired ambidextrous trait definitions under scala.collection
.
So now I have
//> using scala 3.3.1
trait Hat:
def tickets : Set[String]
object HatApp:
import scala.collection.{immutable,mutable}
import scala.util.Random
val hat = new Hat:
def tickets : Set[String] = Set("Alice", "Bob", "Carol")
@main
def winner = println( Random.shuffle(hat.tickets.toList).head )
and everything works as expected.
The import is superfluous, gratuitous, unnecessary in this case. Since the only collection I touch is the unqualified Set
, I could just have omitted any import.
But collections are very common to use. In real applications, one very often wants to set up ones files for clear, easy access to them. I think this is a good syntax to use, that lets on both write out clear collection names and avoid surprising ambiguities.