6

I'm trying to understand singleton types in shapeless and faced misunderstanding about singleton types compile-time type. Here is an example:

val x: Witness.`120`.T = 120.narrow

It works fine, but this constructions looks very unusual. What is Witness.120? In IDE it points to some macro function selectDynamic:

def selectDynamic(tpeSelector: String): Any = macro SingletonTypeMacros.witnessTypeImpl

which has compile-time type Any and judging by the construction Witness.120.T a type member T. This looks like magic... Can anyone give some explanation on what actually is going on when one writes something like:

val x: Witness.`120`.T = //...
Krzysztof Atłasik
  • 21,985
  • 6
  • 54
  • 76
Some Name
  • 8,555
  • 5
  • 27
  • 77
  • 3
    The explanation is a bit more complex and I hope someone will write a more detailed answer later. But for now, let me give you a simplification. Scala's type system knew about singleton types since _(almost)_ is very first versions, for example the types of `objects`. However, for literals _(e.g. `120`)_ we didn't have a way to express and directly use such types in code until `2.13`. What the shapeless witness provides for previous versions of Scala is a way to access those. It works as a **macro** which generates a new instance of the class `Witness` whose **type parameter** `T` is singleton – Luis Miguel Mejía Suárez Dec 16 '19 at 11:38
  • @LuisMiguelMejíaSuárez _It works as a macro which generates a new instance of the class Witness whose type parameter T is singleton_ Are you sure it generates a subtype of `Witness`. I ran some experiments and from a compile error I saw it seemed it was `AnyRef{type T = Int(120); type ->>[V(in type ->>)] = V(in type ->>) with shapeless.labelled.KeyTag[Int(120),V(in type ->>)]; type Field[V(in type Field)] = V(in type Field) with shapeless.labelled.KeyTag[Int(120),V(in type Field)]}`. Pretty crazy type definition... – Some Name Dec 16 '19 at 13:19
  • Yeah, the type is somewhat complex and to be honest, I am not really sure if it is a subtype of a witness or just an anonymous class. The point is, the macro is able to access the internals of the compiler, between those it could find the singleton type of `120` _(Here: `type T = Int(120)`)_ and to let you use it, it creates a new instance of a class which has a **type member** _(type parameter was a typo in my previous comment)_ which contains such type so you can access it. The select dynamic is another complex part, I would not really dive deep to understand how it works. – Luis Miguel Mejía Suárez Dec 16 '19 at 13:49

1 Answers1

5

Witness creates a so-called literal-based singleton type. Literal type means it's a type that can only accept one value.

So if you create a function like this:

def f(x: Witness.`120`.T) = x

it would accept only integer 120, but not 121.

Since Scala 2.13 literal types are integrated into the language, so you can write it simply as:

def f(x: 120) = x

Function narrow narrows type of value 120 from general Int to literal 120.

Krzysztof Atłasik
  • 21,985
  • 6
  • 54
  • 76