Warm tip: This article is reproduced from serverfault.com, please click

Behaviour of context bounds and implicit parameter lists with regards to path dependent types

发布于 2020-11-29 15:53:55

I always thought that context bounds and implicit parameter lists behaved exactly the same, but apparently not.

In the example below, I expect summon1[Int] and summon2[Int] to return the same type, but they don't. I expected summon2[Int] to return a path dependent type, but instead it gives me a type projection. Why?

Welcome to the Ammonite Repl 2.2.0 (Scala 2.13.3 Java 11.0.2)
@ trait Foo[A] {
    type B
    def value: B
  }
defined trait Foo

@ implicit def fooInt = new Foo[Int] {
      override type B = String
      override def value = "Hey!"
    }
defined function fooInt

@ implicit def fooString = new Foo[String] {
      override type B = Boolean
      override def value = true
    }
defined function fooString

@ def summon1[T](implicit f: Foo[T]) = f.value
defined function summon1

@ def summon2[T: Foo] = implicitly[Foo[T]].value
defined function summon2

@ summon1[Int]
res5: String = "Hey!"

@ summon2[Int]
res6: Foo[Int]#B = "Hey!"

@
Questioner
gogstad
Viewed
0
Dmytro Mitin 2020-11-30 02:04:41

The thing is primarily not in the difference of context bound vs. implicit parameter (there shouldn't be any difference (*)), the thing is that implicitly can break type of implicit found

https://typelevel.org/blog/2014/01/18/implicitly_existential.html

If you fix summon2 using custom materializer this will work as expected

def materializeFoo[T](implicit f: Foo[T]): Foo[T] { type B = f.B } = f

def summon2[T: Foo] = materializeFoo[T].value

summon2[Int]
// val res: String = Hey!

It's intersting that shapeless.the doesn't help

def summon2[T: Foo] = the[Foo[T]].value

summon2[Int]
// val res: Foo[Int]#B = Hey!

Also in Scala 2.13 you can use more general form of materializer (not specific for Foo) returning singleton type (like it's done in Scala 3)

def materialize[A](implicit f: A): f.type = f

def summon2[T: Foo] = materialize[Foo[T]].value

val y = summon2[Int]
// val res: String = Hey!

(*) Well, there is a difference that if you don't introduce parameter name f you can't refer to the type f.B explicitly in the return type. And if you don't specify return type explicitly, as we can see such type f.B can't be inferred because of the lack of a stable prefix f (see also Aux-pattern usage compiles without inferring an appropriate type).