41

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?

pathikrit
  • 32,469
  • 37
  • 142
  • 221
  • 1
    What does "widen" mean in the commit message: `Apparently an empty refinement means 'don't widen me'.`? – Kevin Meredith Mar 09 '16 at 21:43
  • Widening is referring to implicit type widening (e.g., List(1, 2.0) is a List[Double] because the int 1 can be widened to double to match 2.0). This gist might help explain it: https://gist.github.com/milessabin/65fa0d4ef373781d3ab4 – Robert Horvick Mar 09 '16 at 22:18
  • Widen means widen type of `object X` from singleton type `X.type`, since usually you want to avoid inferring singleton types, but here the singleton type is desired. – som-snytt Mar 09 '16 at 22:18
  • Where is this feature documented in the Scala spec? I did not find anything about this anywhere except in the Shapeless code.., – pathikrit Mar 09 '16 at 22:21
  • The feature of not inferring singleton is specked, but I don't think explicit empty refinement is specked. http://www.scala-lang.org/files/archive/spec/2.11/06-expressions.html#local-type-inference – som-snytt Mar 10 '16 at 02:19

1 Answers1

54

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.

Miles Sabin
  • 23,015
  • 6
  • 61
  • 95
  • First, equivalent types should behave identically and should in fact have the same representation, so I do find this surprising and evil. But I'm confused by the behavior: I'm guessing widening gets applied to `"foo".type {}` and fails to widen it, is that it? Could one hide `T {}` behind a macro annotation `T @narrow`? – Blaisorblade Mar 10 '16 at 18:41
  • Yes, that's right. I really don't see any reason for thinking that a macro annotation is superior to using a piece of existing, but otherwise unused syntax. – Miles Sabin Mar 10 '16 at 20:20
  • Thanks for the great explanation @MilesSabin – pathikrit Mar 11 '16 at 00:26
  • 1
    @MilesSabin: a macro annotation could have a self-explanatory name, unlike this piece of syntax ;-). – Blaisorblade Mar 12 '16 at 19:55
  • 1
    And how did someone discover this hidden feature? – Jasper-M Aug 01 '16 at 08:58
  • 3
    In my case, completely by accident. – Miles Sabin Aug 02 '16 at 09:41
  • I do agree, I will use a macro from now on so my peers don't get confused. I can always add a small comment inside the macro with the link to this question – gurghet Feb 28 '19 at 22:47