What does `T {}` do in Scala

前端 未结 1 890
悲哀的现实
悲哀的现实 2020-12-23 16:54

Browsing Shapeless code, I came across this seemingly extraneous {} here and here:

trait Witness extends Serializable {
  type T
  val value: T          


        
相关标签:
1条回答
  • 2020-12-23 17:05

    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.

    0 讨论(0)
提交回复
热议问题