Unexpected implicit resolution based on inference from return type
Asked Answered
P

1

50

Given a typeclass where instance selection should be performed based on the return type:

case class Monoid[A](m0: A) // We only care about the zero here
implicit def s[T] : Monoid[Set[T]] = Monoid(Set.empty[T])
implicit def l[T] : Monoid[List[T]] = Monoid(List.empty[T])
def mzero[A](implicit m: Monoid[A]) : A = m.m0

why does Scala (2.11.6) fail to resolve the proper instance:

scala> mzero : List[Int]
<console>:24: error: ambiguous implicit values:
 both method s of type [T]=> Monoid[Set[T]]
 and method l of type [T]=> Monoid[List[T]]
 match expected type Monoid[A]
              mzero : List[Int]
              ^

when it has no problems finding an implicit based on the return type when using the implicitly function (we redefine it here as i to illustrate how similar it is to mzero)

def i[A](implicit a : A) : A = a
scala> i : Monoid[List[Int]]
res18: Monoid[List[Int]] = Monoid(List())

The Monoid[A], instead of Monoid[List[Int]] in the error message is puzzling.

I would assume many scalaz contributors to be familiar with this problem as it seems to limit the convenience of typeclasses in scala.

EDIT: I'm looking into getting this working without forgoing type inference. Otherwise I'd like understand why that's not possible. If this limitation is documented as a Scala issue, I could not find it.

Pekin answered 1/6, 2015 at 16:9 Comment(4)
The limits of Scala's ability to use expected return types to guide inference of type parameters when implicits are involved are incredibly confusing, so everybody just writes mzero[List[Int]].Tewell
As some workaround you can get away by redefining mzero as def mzero[A](implicit m: Monoid[_<:A]) : A = m.m0. Ut I'd personnaly go with mzero[List[Int]] as Travis Brown mentioned.Dean
Impressive and surprising. Thanks! Any explanation? This appears to be relaxing the type and obtaining fewer matches!Aquiculture
Sorry but no, I have no explanation of this behavior. It's just that experience told me to try to make Monoid covariant, which indeed makes the code compile. As you probably cannot make it covariant (given that a complete definition of Monoid would have occurences of A in contravariant position), the next best thing is to try to emulate this on mzero alone by using an existential type, giving def mzero[A](implicit m: Monoid[_<:A]). I would definitely be interested in getting an explanation of this oddness myself.Dean
B
1

1) After rewriting your code as follows:

case class Monoid[A](m0: A) // We only care about the zero here
implicit def s[T] : Monoid[Set[T]] = Monoid(Set.empty[T])
implicit def l[T] : Monoid[List[T]] = Monoid(List.empty[T])
def mzero[A]()(implicit m: Monoid[A]) : A = m.m0

val zero = mzero[List[Int]]()
val zero2: List[Int] = mzero()

then becomes clearly why that works so.

2) After you had to set mzero as def mzero[A]()(implicit m: Monoid[_ <: A]) : A = m.m0 then you enabled additional type inference to resolve existencial type. Compiler got actual type from required return type. You could check it with def mzero[A <: B, B]()(implicit m: Monoid[A]) : A = m.m0 if you want.

3) Of course all that behaviour is just subtleties of compiler and I not think that such partial cases really required deep understanding.

Blotto answered 24/8, 2016 at 8:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.