Custom literal-based singleton types

Is it possible to define custom types whose literals can be subtypes of Singleton (see documentation)? To make it concrete, how could the definition of SingletonType be modified in the example below so that it compiles?

final case class SingletonType[T <: Singleton](t: T)

// foo represents a context where a singleton type is required
def foo[S <: Singleton](s: S): s.type = ???

foo(0) // No compile-error
foo(SingletonType(0)) // Compile-error

Note: using an intermediate val is not possible in many less minimized and more meaningful cases.

The problem is that I can create as many SingletonType(0) as I want, making it not a singleton.

Well, they are all equivalent. Conceptually, there is only one structure. And the code can easily be changed to preclude multiple copies, via hash consing. Then the problem is how to tell the compiler that they are singletons…

Can you give us more details on why the compiler needs to know these are singletons?

This sounds like a place where enums would do fine if you know how many there will be in advance. If not then maybe a Map.withDefault( key => {construct and update with (key) }) would do the job.

Here is a toy example. The idea is to have the Scala type checker validate scoped declarations. This can be useful to faithfully represent an abstract syntax in Scala.

A singleton type can have only one data inhabitant. Your type definition isn’t a singleton type because you can instantiate multiple "SingletonType"s.

You can fix this by changing to a case object, or plain object. Object définitions are singletons.

object A //singleton value 
val a: A.type = A//singleton type whose sole occupant is A`
1 Like

But I want infinitely many… and I want them to have arbitrarily complex structure…

final case class R[T1 <: Singleton, ..., Tn <: Singleton]
(t1: T1, ..., tn: Tn) extends Singleton
// Compiler auto-generates a final constructor
// that makes sure there is only one instance of each type.

There are infinitely many ints and Strings, each with their own singleton type. It looks like Scala forgot to make singleton types extensible, bringing us back to coding with only a fixed number of primitive types, like in some older version of Fortran.

:face_with_peeking_eye:

sips are tldr; but the spec is pithy on singleton and literal types.

Literal types are available for all types for which there is dedicated syntax except Unit.

I see it also excepts (not accepts) old-style Symbol literals, f('x).

1 Like

Int is the super type of the singletons 1, 2, 3, etc. The equivalent for you would be to define infinite objects extending your Singleton trait. A class will never be recognized as a singleton, because someone can always instantiate more than one.

I get you’re saying it’s currently impossible. But it doesn’t have to remain so. Case classes together with compiler-recognized hash consing can ensure that there is at most one instance of each type, and allow that instance to be retrieved from its type.

object R:
  def apply[T1 <: Singleton, ..., Tn <: Singleton](t1: T1, ..., tn: Tn): R[T1, ..., Tn] =
    synchronized(instances.getOrElseUpdate((t1, ..., tn), new R(t1, ..., tn)))
      .asInstanceOf[R[T1, ..., Tn]]
  private val instances = mutable.WeakHashMap.empty[(Any, ..., Any), R[?, ..., ?]]

(Scastie link with the full code)

This way, no one can instantiate more than one (except via reflection).

The latest version of the spec is slightly more detailed.

I think what you’re actually missing here is a way to specify a trait that says that all of its children must either singletons or traits whose children are singletons.

If we had such a concept in Scala, we could express your R example like so:

singleton trait Singleton
singleton trait R[T1 <: Singleton, T2 <: Singleton](t1: T1, t2: T2) extends Singleton 
object A extends Singleton
object B extends Singleton

object RImpl extends R(A, B)
object RImpl2 extends R(RImpl, B)

This would not necessitate allowing classes to be singletons, while still letting you achieve your dream of having infinite singleton definitions. You would still have the ability to invoke a constructor (the trait constructor in this case), and at the same time would have the same guarantee that your singleton is truly singleton that we currently have in Scala.

1 Like

If I understand correctly, for traits extending Singleton, the compiler would need to do an additional check to disallow code as below, right?

object RImpl1 extends R(A, B)
object RImpl2 extends R(A, B)
// R[A.type, B.type] now has two instances

Also, do you envision the singleton keyword as syntactic salt to clarify the special semantics of extending Singleton? Or is it intended to have some additional meaning?

No, such code would be allowed because RImpl1 and RImpl2 are both still singletons in that example. They each have their own specific type that they are the sole-inhabitant of.

But then, what would constValue[R[A.type, B.type]] return?

what is the issue with using object? that is user extensible, can extend traits, and is always a Singleton

You can’t get a constValue from R, because there is none. constValue can only summon singletons, and R is not a singleton. If you made R sealed, you could summon its constant children though.

objects do not take type parameters (and rightfully so), so their type does not have any structure. Only a finite number of them can be statically declared. They cannot be anonymous (unlike strings); they are not literals.

I would like to be able to define my own literal type families, with arbitrary structure. For example, a literal type family isomorphic to the types of tuples (*:[H, T <: Tuple]) could be handy.

You can see in the example I shared earlier that it is currently impossible in Scala to pass a tuple to Var as its nameAndType, because the latter is not a subtype of Singleton.

It might be possible to simulate what I want by encoding all other types into strings, using some kind of encoding, but that is obviously not ideal.

import scala.compiletime.ops.any.ToString
import scala.compiletime.ops.string.+

type Encoded[T] <: String = T match
  case EmptyTuple => "EmptyTuple"
  case h *: t => "Tuple(" + Encoded[h] + ", " + Encoded[t] + ")"
  case String => "String(" + ToString[T] + ")"
  case _ => ToString[T]

They could jettison object keyword which really means module and not merely instance, and instead use singleton which behaves more like given, perhaps with similar syntax, which can be parameterized and anonymous, but to create a singleton.

Scala 4 is not that far away!