Search code examples
scalascala-cats

Option[io.databaker.env.EnvValue], but type F is invariant in type


I have the following code snippet, that does not get compiled:

trait Environment[F[_]] {
  def get(v: EnvVariable): F[Option[EnvValue]]
}

final class LiveBadEnvironment[F[_] : Sync] extends Environment[F] {
  override def get(v: env.EnvVariable): F[Option[env.EnvValue]] = None.pure[F]
}

the compiler complains:

[error]  found   : F[None.type]
[error]  required: F[Option[io.databaker.env.EnvValue]]
[error]     (which expands to)  F[Option[io.databaker.env.EnvValue.Type]]
[error] Note: None.type <: Option[io.databaker.env.EnvValue], but type F is invariant in type _.
[error] You may wish to define _ as +_ instead. (SLS 4.5)
[error]   override def get(v: env.EnvVariable): F[Option[env.EnvValue]] = None.pure[F]

What am I doing wrong?


Solution

  • I have changed to override def get(v: env.EnvVariable): F[Option[env.EnvValue]] = F.pure(None) and got the error message not found: value F. What am I doing wrong?

    Consider how the name F is used in implicit F: Applicative[F]

    def foo[F[_]](implicit F: Applicative[F]): F[Option[String]] = F.pure(None)
            |              |              |                        |
          type           value          type                 "type as value"
    

    Note how the value parameter F has the same name as the type parameter F. Now calling a method on value F looks like as if we are calling a method on a type

    F.pure(None)
    

    Invoking a method on a type using dot syntax is not possible in Scala, but conceptually that is what we are doing - we wish to convey the idea of invoking a "type-level" function. This naming convention works because values and types live in two separate universes so we can reuse the same name without clashes. For example, consider why the following is legal

    scala> object f { def f[f](f: f): f = f }
         | val Int: Int = 42
    object f
    val Int: Int = 42              
    

    Now when using context bound : notation

    def foo[F[_]: Applicative]: F[Option[String]] = Applicative[F].pure(None)
    

    we do not have the name of implicit value parameter to work with, so we cannot use the above convention trick and call

    F.pure(None)
    

    because, again, dot notation on types is strictly speaking illegal, so instead we use the companion object with main method trick

    Applicative[F].pure(None)
    

    This works because Applicative companion has something like

    Applicative {
      def apply[F[_]](implicit instance: Applicative[F]): Applicative[F] = instance
    }
    

    so calling

    Applicative.apply[F]
    

    or shorter

    Applicative[F]
    

    returns the implicit instance in scope. At this point we do have our value to work with and so dot notation becomes legal

    Applicative[F].pure(None)
                  |
        ok because invoked on a value
    

    Therefore, you have to call with Sync[F].pure(None) instead of F.pure(None), because in your particular case, you are using context bounds.