Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Covariant HKT implicits fail to resolve #11427

Open
kaishh opened this Issue Mar 8, 2019 · 10 comments

Comments

Projects
None yet
4 participants
@kaishh
Copy link

kaishh commented Mar 8, 2019

Given this setup:

object example extends App {
trait MonoIO[F[_]]
trait BifunctorIO[F[+_, _]]

case class IO[+A]()
object IO {
  implicit val monoInstance: MonoIO[IO] = new MonoIO[IO]{}
}

trait AnyIO[+F[_]]
object AnyIO {
  implicit def fromMono[F[_]: MonoIO]: AnyIO[F] = new AnyIO[F]{}
  implicit def fromBIO[F[+_, _]: BifunctorIO]: AnyIO[({ type l[A] = F[Nothing, A]})#l] = new AnyIO[({ type l[A] = F[Nothing, A]})#l]{}
}

class SomeAlg[+F[_]]
type SomeAlg2[F[_, _]] = SomeAlg[({ type l[A] = F[Nothing, A]})#l]
object SomeAlg {
  def make[F[_]: AnyIO](): SomeAlg[F] = new SomeAlg[F]
}

val alg: SomeAlg[IO] = SomeAlg.make[IO]()
val alg1: SomeAlg[IO] = SomeAlg.make()
}

Constructing SomeAlg fails even when the types are fully annotated:

val alg: SomeAlg[IO] = SomeAlg.make[IO]()
// could not find implicit value for evidence parameter of type AnyIO[IO]

Explicitly providing AnyIO instance works though: SomeAlg.make[IO](AnyIO.fromMono[IO])

Removing covariance from AnyIO allows SomeAlg.make[IO]() to work, however, a non-type annotated version will still fail to resolve:

val alg1: SomeAlg[IO] = SomeAlg.make()

This is only fixed if covariance is removed from SomeAlg too.
The reason for wanting covariance on these classes is to represent errorless effects, e.g. Clock, Random, Logging:

trait Clock[+F[_]]
trait Clock2[F[_, _]] = Clock[F[Nothing, ?]]

def clockExecutionTime[F[_]: Clock, A](f: F[A]): F[(Duration, A)] = ???

def potentiallyErroringAction[F[_, _]]: F[Throwable, Unit] = ???

def clockedAction[F[+_, +_]: Clock2]: F[Throwable, Unit] = {
  clockExecutionTime(potentiallyErroringAction[F])
}

^ The covariance on the example clock algebra is extremely convenient as it allows it to transition seamlessly between bifunctor (arbitrary error type) and monofunctor (fixed error type) contexts. However, with the bug above, these algebras are extremely limited in how they can participate in implicit resolution.

@joroKr21

This comment has been minimized.

Copy link

joroKr21 commented Mar 9, 2019

I think the problem boils down to:

  1. Companion objects are only in implicit scope for their corresponding type, not subtypes or supertypes.
  2. Covariance causes the compiler to look for MonoIO[F] for some F[x] <: IO[x] not MonoIO[IO]

Note that F[_] is a type variable and it doesn't have a companion object.
Putting the instance in the companion object of MonoIO works:

trait MonoIO[F[_]]
trait BifunctorIO[F[+_, _]]
case class IO[+A]()
object MonoIO {
  implicit val monoInstance: MonoIO[IO] = new MonoIO[IO]{}
}

trait AnyIO[+F[_]]
object AnyIO {
  implicit def fromMono[F[_]: MonoIO]: AnyIO[F] = new AnyIO[F]{}
  implicit def fromBIO[F[+_, _]: BifunctorIO]: AnyIO[({ type K[A] = F[Nothing, A] })#K] = new AnyIO[({ type K[A] = F[Nothing, A] })#K]{}
}
@kaishh

This comment has been minimized.

Copy link
Author

kaishh commented Mar 9, 2019

@joroKr21
Point 1 shouldn't be correct since according to this article the rules are:

The implicit scope of a type T consists of all companion modules (§5.4) of classes that are associated with the implicit parameter’s type. Here, we say a class C is associated with a type T , if it is a base class (§5.1.2) of some part of T

Meaning all supertypes of all type parameters, projection/selection prefixes, intersections and of the type itself are ALL searched. And also this example works, showing that the companion of the supertype must have been searched:

object example extends App {
  class C[+A]

  trait TBase
  object TBase {
    implicit def CTBase[T <: TBase]: C[T] = new C[T]{}
  }

  class T extends TBase

  implicitly[C[T]]
  def x[X <: TBase] = implicitly[C[X]]
  implicitly[C[X forSome { type X <: TBase}]]
}

Point 2 is the bug in question indeed, the -Xlog-implicits does mention F. I don't understand why putting it into MonoIO companion works, perhaps the IO companion is not searched because higher kinded types are treated differently here? I think the search should never be narrowed to F[x] <: IO[x] which is a far wider range of types than IO[x], I think for covariance the search should be for F[x] forSome { type F[x] <: IO[x] } (which succeeds) not for F[x] <: IO[x]

@joroKr21

This comment has been minimized.

Copy link

joroKr21 commented Mar 9, 2019

@kaishh Ah yes, you are right, I completely missed that part of implicits for some reason.
Your second example works just the same with higher-kinded types.

So I guess the problem is that while in your second example the fact that X <: TBase is determined by the upper bound, in the first one this fact is only known by a type variable constraint (AnyIO[F] <: AnyIO[IO] => F[x] <: IO[x]) and maybe the constraints are not considered base types. I don't have enough knowledge to say whether it's sound and/or feasible algorithmically to consider type variable constraints for implicit scope as well.

Edit: Your original example doesn't work even if the type is not higher-kinded.

@kaishh

This comment has been minimized.

Copy link
Author

kaishh commented Mar 11, 2019

@joroKr21
Note that my original example works in dotty: https://scastie.scala-lang.org/0cq8ypISSH2jBXNNEqtRlg
So, I think the behavior seen here is unlikely to be a deliberate optimization since Dotty went ahead with supporting this.
Sorry for bothering, but, @smarter, any ideas what could it be that dotty does correctly here that scalac doesn't?

@smarter

This comment has been minimized.

Copy link

smarter commented Mar 11, 2019

Don't know, but what @joroKr21 is saying seems plausible, Dotty does go through upper bounds of type variables to decide what companions to look for to find implicits, I think Scala 2 does the same, but maybe it doesn't do it recursively or something ?

@smarter

This comment has been minimized.

Copy link

smarter commented Mar 11, 2019

(Also I don't recommend using Scastie to infer Dotty's behavior because it's stuck on an old version of Dotty because of scalacenter/scastie#281)

@kaishh

This comment has been minimized.

Copy link
Author

kaishh commented Mar 11, 2019

Works on dotty-0.10.0-RC1 too.

I think Scala 2 does the same, but maybe it doesn't do it recursively or something ?

Ok, would that be hard to fix? And if, hypothetically, there was a fix, would it be accepted into 2.12(.9, .10) series or would that be considered a breaking change?

@smarter

This comment has been minimized.

Copy link

smarter commented Mar 11, 2019

No idea about how hard it'd be to fix :). Probably unlikely to get into 2.12 since it could break source compatibility in non-obvious ways.

@kaishh

This comment has been minimized.

Copy link
Author

kaishh commented Mar 17, 2019

This works on 2.12 with -Xsource:2.13 flag and on 2.13.0-pre-f1dec97.
This might have been another bug closed by scala/scala#6069
I guess it can be closed?

@kaishh kaishh closed this Mar 17, 2019

@SethTisue SethTisue added this to the 2.13.0-RC1 milestone Mar 17, 2019

@SethTisue SethTisue added the typer label Mar 17, 2019

@kaishh

This comment has been minimized.

Copy link
Author

kaishh commented Mar 18, 2019

Unfortunately, that was a false positive. The example DOES NOT work under any of 2.13.0-M5, 2.13.0-pre-dd34f62, 2.12.8 with -Xsource:2.13. I was fooled by copypasting the setup code forgetting the summon itself...
I'm reopening then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.