I was chatting with @adriaanm earlier about the new scala.Singleton marker trait which is slated to be included in Scala 2.13, and thought this forum would be a better place to hold that discussion.
I was wondering if we could keep Singleton as experimental, behind a language import for 2.13? It seems to me that how it works may need to drastically change, because of issues found when combining singletons and unions (see lampepfl/dotty#1551).
Problem:
I think the fundamental issue is that Singleton is not an appropriate kind of type to be considered an upper bound, as described in lampepfl/dotty#4944
My reason for thinking Singleton shouldn’t be an upper bound is that (concrete) types behave like sets. So the following types could be described:
Int = {..., -2, -1, 0, 1, 2, ... }
1.type = { 1 }
2.type = { 2 }
and that <: behaves exactly like subsetOf in sets. We definitely have the results that 1 <: Int, 2 <: Int and 1 | 2 <: Int because all of 1, 2 and 1 | 2 are subsets of Int, because
1.type = { 1 } subsetOf {..., -2, -1, 0, 1, 2, ... }
and
2.type = { 2 } subsetOf {..., -2, -1, 0, 1, 2, ... }
and
1 | 2 = { 1 } U { 2 } = { 1, 2 } subsetOf {..., -2, -1, 0, 1, 2, ... }
However, Singleton describes the set of of all types that contain a single value. So
really, Singleton is more like:
Singleton = { { 1 }, { 2 }, { 3 }, { "hello" }, { "world" }, { 'a' }, ... }
Notice that the 1.type = { 1 } is not a subset of this set, rather it is an element of it.
Therefor, 1 <: Singleton violates this idea that A <: B if and only if A subsetOf B, and this is why IMO it interacts poorly with union types. If you do not violate this property, then you do not break union types. For example, we have:
Int <: AnyVal and Char <: AnyVal
therefor:
Int | Char <: AnyVal
this works because Int is a real subset of AnyVal.
If we instead use Singleton in supertype position:
1 <: Singleton and 2 <: Singleton
therefor:
1 | 2 <: Singleton
but {1, 2} is not a subset of Singleton! { { 1 }, { 2 } } is a subset of Singleton,
but that is not the set described by 1 | 2.
Solution
I don’t see how this mixing of concepts could ever be reconciled, and I hope its inclusion won’t shut the door permanently on allowing singletons to participate in Unions. That would be an enormous missed opportunity (see: TypeScript)!
I think the solution is to hold our horses on allowing users to specifically name the type Singleton, especially as an upper bound. Perhaps it could be enabled with a compiler flag, or behind a language feature import, and treat it as experimental i.e. may change in 2.14 or 3.0.
I think that this idea of singleton-ness could be better expressed via a typeclass, and in fact we already have one such typeclass, it is called ValueOf[T]
Although ValueOf[T] is more general than just Singleton types – users can create their own instances by:
implicit val valueOfString = ValueOf[String]("Hello")
– if a restriction is desired for only specifically Singleton types, then perhaps a Singleton[T] typeclass could be created which works in the same way ValueOf[T] does, but which is not instantiable by users (i.e. it has a private constructor).
Thank you for your time!