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

1

1

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!"

@
Panfish answered 29/11, 2020 at 15:53 Comment(0)
P
4

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).

Privative answered 29/11, 2020 at 15:58 Comment(2)
Ah, I see. So in other words, summon1 works because there is a path to the type, via the f handle, but in summon2 there isn't. Got it 👍Panfish
@Panfish note that in general (of course there are exceptions) having implicit values and methods without explicit return types is considered a bad practice. Having explicit return types can help inference and avoid introducing bugs with changes.Belda

© 2022 - 2024 — McMap. All rights reserved.