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

Proposal: Automatically include implicits in ModuleDefs? #230

Closed
kaishh opened this issue Jun 29, 2018 · 1 comment

Comments

Projects
2 participants
@kaishh
Copy link
Member

commented Jun 29, 2018

We could leave out explicit bindings of type classes in DI context, so instead of this:

trait Module[F[_]: TagK: Monad] extends ModuleDef {
  make[Monad[F]].from(Monad[F])

  make[MyService[F]]
}

We could write this:

trait Module[F[_]: TagK: Monad] extends ModuleDef {
  make[MyService[F]]
}

For static (macro) provisioner the change is easy, since generated code has access to the scope and implicits of enclosing ModuleDef: see https://github.com/kaishh/izumi-r2/pull/1/files#diff-994a658fc1f58f4d7831c3113087b6d9 , so we just need to replace parameters with implicitly[X] and not request them from DI context.

trait Module[F[_]: TagK: Monad] extends StaticModuleDef {
  stat[MyService[F]]
}

But, there are problems with this approach:

  • Implicits are not visible in DI context and can't be requested as non-implicit parameters
  • There are no guarantees of type class coherence (coherence means that two instances of a type class for the same type are the same instance everywhere in the program) because instances depend on local scope of module declaration, there may be conflicting instances in DI context, totally unmanaged and invisible to DI.

What we can do instead, is capture implicit parameters and inject them into context with a macro, transforming a call such as this:

make[MyService[F]]

into something like this

make[MyService[F]].withAddedImplicits(implicitly[Monad[F]])

That way, implicits are also managed by DI and we can guarantee type class coherence (which is a desirable feature in global context and which scala itself currently does not guarantee) – if there are two different instances for the same type class, application will fail to start and report conflicting bindings

However, there is an issue with deciding instance equality, for implicit vals everything is fine, but for implicit defs every implicit summon will generate a new instance, these instances are not equivalent:

import cats._
import cats.data._
import cats.effect._

class A[T[_]: Monad](dummy: Boolean = false) {
  val m = Monad[T]
}

object T {
  type opt[A] = OptionT[IO, A]
}
import T._

val m1 = new A[opt]().m
// m1: cats.Monad[T.opt] = cats.data.OptionTInstances$$anon$2@67757fa4

val m2 = new A[opt](false).m

m1 == m2
// false

m1.getClass
// cats.data.OptionTInstances$$anon$2

m1.getClass.isAnonymousClass
// true

m1.getClass == m2.getClass
// true

That means we can't, in general, decide type class instance equivalence and guarantee coherence.

But, we can use some heuristics instead, for example:

  1. We can compare implicits without type parameters (such as ExecutionContext) using .equals, but always assume that implicits with type parameters (such as Monad[F]) are type classes and are the same if their type parameters are the same and compare them only by using SafeType.equals.
  2. We can asssume that all implicit instances that aren't AnonymousClasses are implicit vals and compare them using .equals, but when .getClass.isAnonymousClass is true, compare them only by using .getClass.equals – this heuristic is kinda faulty – while most library authors use traits to define type classses and put new X { ... } anonymous classes in implicit defs, sometimes they also create named classes for instances. Also, this is less portable – scala.js doesn't have .isAnonymousClass method on Class.

@kaishh kaishh added enhancement distage (di) api and removed api labels Jun 29, 2018

@kaishh kaishh self-assigned this Jul 4, 2018

@pshirshov pshirshov added the high-prio label Jul 23, 2018

@pshirshov pshirshov added this to the 0.6 milestone Jul 23, 2018

@pshirshov pshirshov added this to To do in DIStage via automation Sep 30, 2018

@kaishh

This comment has been minimized.

Copy link
Member Author

commented Oct 1, 2018

Added .addImplicit method which is sort of a half-measure:

val mod = new ModuleDef {
  addImplicit[Ordering[Int]]
// same as make[Ordering[Int]].from(implicitly[Ordering[Int]])
}

The reason for not going all the way is that it's actually not modular to require implicit implementations to be available in scope when binding classes depending on implicits.
Requiring implicits to be explicitly bound to implementations seems like a more consistent position to me, since If we'd have wanted to directly depend on implementations, we may as well not have bothered with distage at all.

@pshirshov pshirshov closed this Oct 1, 2018

DIStage automation moved this from To do to Done Oct 1, 2018

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.