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!