Browsing Shapeless code, I came across this seemingly extraneous {}
here and here:
trait Witness extends Serializable { type T val value: T {} } trait SingletonOps { import record._ type T def narrow: T {} = witness.value }
I almost ignored it as a typo since it does nothing but apparently it does something. See this commit: https://github.com/milessabin/shapeless/commit/56a3de48094e691d56a937ccf461d808de391961
I have no idea what it does. Can someone explain?
Here we have defined an abstract type T . It is used to describe the type of element . We can extend this trait in an abstract class, adding an upper-type-bound to T to make it more specific.
def is the keyword you use to define a method, the method name is double , and the input parameter a has the type Int , which is Scala's integer data type. The body of the function is shown on the right side, and in this example it simply doubles the value of the input parameter a : def double(a: Int) = a * 2 -----
Any type can be followed by a {}
enclosed sequence of type and abstract non-type member definitions. This is known as a "refinement" and is used to provide additional precision over the base type that is being refined. In practice refinements are most commonly used to express constraints on abstract type members of the type being refined.
It's a little known fact that this sequence is allowed to be empty, and in the form that you can see in the shapeless source code, T {}
is the type T
with an empty refinement. Any empty refinement is ... empty ... so doesn't add any additional constraints to the refined type and hence the types T
and T {}
are equivalent. We can get the Scala compiler to verify that for us like so,
scala> implicitly[Int =:= Int {}] res0: =:=[Int,Int] = <function1>
So why would I do such an apparently pointless thing in shapeless? It's because of the interaction between the presence of refinements and type inference. If you look in the relevant section of the Scala Language Specification you will see that the type inference algorithm attempts to avoid inferring singleton types in at least some circumstances. Here is an example of it doing just that,
scala> class Foo ; val foo = new Foo defined class Foo foo: Foo = Foo@8bd1b6a scala> val f1 = foo f1: Foo = Foo@8bd1b6a scala> val f2: foo.type = foo f2: foo.type = Foo@8bd1b6a
As you can see from the definition of f2
the Scala compiler knows that the value foo
has the more precise type foo.type
(ie. the singleton type of val foo
), however, unless explicitly requested it won't infer that more precise type. Instead it infers the non-singleton (ie. widened) type Foo
as you can see in the case of f1
.
But in the case of Witness
in shapeless I explicitly want the singleton type to be inferred for uses of the value
member (the whole point of Witness
is enable us to pass between the type and value levels via singleton types), so is there any way the Scala compiler can be persuaded to do that?
It turns out that an empty refinement does exactly that,
scala> def narrow[T <: AnyRef](t: T): t.type = t narrow: [T <: AnyRef](t: T)t.type scala> val s1 = narrow("foo") // Widened s1: String = foo scala> def narrow[T <: AnyRef](t: T): t.type {} = t // Note empty refinement narrow: [T <: AnyRef](t: T)t.type scala> val s2 = narrow("foo") // Not widened s2: String("foo") = foo
As you can see in the above REPL transcript, in the first case s1
has been typed as the widened type String
whereas s2
has been assigned the singleton type String("foo")
.
Is this mandated by the SLS? No, but it is consistent with it, and it makes some sort of sense. Much of Scala's type inference mechanics are implementation defined rather than spec'ed and this is probably one of the least surprising and problematic instances of that.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With