tmccarthy / bfect

Some bifunctor IO type classes
Apache License 2.0
21 stars 2 forks source link

Instances for cats EitherT[IO, _, _] #6

Closed tmccarthy closed 3 years ago

jchapuis commented 3 years ago

Hi there, I was just writing this below, but I'm now running into a variance problem as BifunctorMonad has covariant parameters while cat's EitherT doesn't. Any suggestions?

package com.bestmile.availability.helpers.instances

import au.id.tmm.bfect.{ Bifunctor, BifunctorMonad, BifunctorMonadError }
import cats.data.EitherT
import cats.{ Functor, Monad }

object BifunctorEitherTInstancesImpl {

  class BifunctorInstance[F[_]](implicit functor: Functor[F]) extends Bifunctor[EitherT[F, *, *]] {
    override def biMap[L1, R1, L2, R2](f: EitherT[F, L1, R1])(leftF: L1 => L2, rightF: R1 => R2): EitherT[F, L2, R2] =
      f.bimap(leftF, rightF)

    override def rightMap[L, R1, R2](f: EitherT[F, L, R1])(rightF: R1 => R2): EitherT[F, L, R2] = f.map(rightF)

    override def leftMap[L1, R, L2](f: EitherT[F, L1, R])(leftF: L1 => L2): EitherT[F, L2, R] = f.leftMap(leftF)
  }

  class BifunctorMonadInstance[F[_]](implicit monad: Monad[F])
    extends BifunctorInstance[F]
    with BifunctorMonad[EitherT[F, *, *]] {
    override def rightPure[A](a: A): EitherT[F, Nothing, A] = EitherT.pure(a)

    override def leftPure[E](e: E): EitherT[F, E, Nothing] = EitherT.leftT(e)

    override def flatMap[E1, E2 >: E1, A, B](fe1a: EitherT[F, E1, A])(
      fafe2b: A => EitherT[F, E2, B]
    ): EitherT[F, E2, B] = fe1a.flatMap(fafe2b)

    override final def tailRecM[E, A, A1](a: A)(f: A => EitherT[F, E, Either[A, A1]]): EitherT[F, E, A1] =
      Monad[EitherT[F, E, *]].tailRecM(a)(f)
  }

  class BMEInstance[F[_]](implicit monad: Monad[F])
    extends BifunctorMonadInstance[F]
    with BifunctorMonadError[EitherT[F, *, *]] {
    override def handleErrorWith[E1, A, E2](fea: EitherT[F, E1, A])(f: E1 => EitherT[F, E2, A]): EitherT[F, E2, A] =
      fea.leftFlatMap(f)
  }

}

trait LowPriorityEitherTInstances {
  implicit def biFunctorInstance[F[_]](implicit functor: Functor[F]): Bifunctor[EitherT[F, *, *]] =
    new BifunctorEitherTInstancesImpl.BifunctorInstance[F]()
}

trait MiddlePriorityEitherTInstances extends LowPriorityEitherTInstances {
  implicit def biFunctorMonadInstance[F[_]](implicit monad: Monad[F]): BifunctorMonad[EitherT[F, *, *]] =
    new BifunctorEitherTInstancesImpl.BifunctorMonadInstance[F]()
}

trait HighPriorityEitherTInstances extends MiddlePriorityEitherTInstances {
  implicit def bmeInstance[F[_]](implicit monadError: Monad[F]): BifunctorMonadError[EitherT[F, *, *]] =
    new BifunctorEitherTInstancesImpl.BMEInstance()
}

object BifunctorEitherTInstances extends HighPriorityEitherTInstances

Btw. my domain looks great using Bifunctor, it reduced a lot of the noise associated with EitherT. Now have to figure a way to make it run 😉

tmccarthy commented 3 years ago

Yeah the variance problem isn't nice. It also creates a mess when you're trying to use the classes in a tagless style. You generally need F[+_, +_]: Sync instead of the nicer F[_, _]: Sync.

I might have a try at making the entire class hierarchy invariant in both type parameters, and add utility methods for the common widening operations. I'll do it on a branch and see what it looks like. I can't think of another way to define instances for EitherT.

jchapuis commented 3 years ago

Yes. On the bright side, however, not having to explicitly widen reduces the noise. Wartremover is getting upset however, I have to disable Wart.JavaSerializable, Wart.Product, Wart.Serializable, Wart.Any.

Seems I'm kind of stuck now without indeed an invariant hierarchy. How about unchecked variance? Unfortunately, the snippet below doesn't work, but maybe you would know a way around it?

class BifunctorMonadInstance[F[_]](implicit monad: Monad[F])
    extends BifunctorInstance[F]
    with BifunctorMonad[EitherT[F, * @uncheckedVariance, * @uncheckedVariance]]
tmccarthy commented 3 years ago

Yeah, kind-projector struggles with unchecked variance. You'd have to create a dedicated type alias like:

type UncheckedEitherT[F[_], A, B] = EitherT[F, A @uncheckedVariance, B @uncheckedVariance]

See example. I'm not sure if the implicit resolution would still work though. I'd be interested to see if this is successful. Please feel free to submit a PR, even if it's just to demonstrate what these EitherT instances would roughly look like.

jchapuis commented 3 years ago

I've tried. No luck.

With this:

type CovariantEitherT[F[_], +A, +B] = EitherT[F, A @uncheckedVariance, B @uncheckedVariance]

The declaration

class BifunctorMonadInstance[F[_]](implicit monad: Monad[F])
    extends BifunctorInstance[F]
    with BifunctorMonad[CovariantEitherT[F, +*, +*]]

Still gives me

covariant type β$2$ occurs in invariant position in type [+β$2$, +γ$3$]cats.data.EitherT[F,β$2$,γ$3$] of value <local Λ$> with BifunctorMonad[CovariantEitherT[F, +*, +*]] {

If I do

class BifunctorMonadInstance[F[_]](implicit monad: Monad[F])
    extends BifunctorInstance[F]
    with BifunctorMonad[CovariantEitherT[F, *, *]]

funnily enough, I get

[β$2$, γ$3$]cats.data.EitherT[F,β$2$,γ$3$]'s type parameters do not match type F's expected parameters: type β$2$ is invariant, but type _ is declared covariant, type γ$3$ is invariant, but type _ is declared covariant with BifunctorMonad[CovariantEitherT[F, *, *]]

looks like the compiler is contradicting itself 😉

tmccarthy commented 3 years ago

Yeah this is very frustrating. The @uncheckedVariance trick should work, and in fact it does work in Scala 2.12. It looks like there's a compiler bug. The following compiles for me in 2.12.12, but not in 2.13.4:

implicit def standardInstances[F[_] : cats.Monad]: BifunctorMonad[({type L[@uncheckedVariance +X, @uncheckedVariance +Y] = cats.data.EitherT[F, X @uncheckedVariance, Y @uncheckedVariance] @uncheckedVariance})#L @uncheckedVariance] =
    ???

As you can see I've put @uncheckedVariance everywhere in the hope that it might work. 😐

tmccarthy commented 3 years ago

There were some issues introduced in 2.13.2 that broke @uncheckedVariance in higher-kinded types (see bugs), but I think these seem to have been fixed in 2.13.3? I might have to raise an issue 😕.

jchapuis commented 3 years ago

Interesting, yeah seems like this is on the fringe and regularly gets broken. That wouldn't be needed with your new invariant branch, however, right? how is it going, do you need any help?

tmccarthy commented 3 years ago

Yes, but unfortunately I'm having other issues with the invariant branch 😅. The implicit conversions for the Ops traits are coming up against another compiler bug causing issues where either type parameter is Nothing, which is a common scenario for this project. I believe I can find a way around it though. Feel free to raise any PRs against the invariant-type-parameters branch. I'll hopefully have some time to work on it this coming weekend.

tmccarthy commented 3 years ago

@jchapuis I've merged the invariant type parameters branch, and am working on instances for EitherT on cats-eithert-instances. Should have something released before Christmas.

jchapuis commented 3 years ago

This is great, thanks a lot 🎅 ! Just to show a sign that I care, I created this tiny PR yesterday night https://github.com/tmccarthy/bfect/pull/16 but feel free to ignore 😄

tmccarthy commented 3 years ago

@jchapuis I've released 0.4.0, which makes instances for EitherT available with either of the following imports:

import au.id.tmm.bfect.interop.cats.instances.eitherT._
// OR
import au.id.tmm.bfect.interop.cats.implicits._

Let me know if you run into any issues!

tmccarthy commented 3 years ago

Turns out I stuffed up the publishing for 0.4.0. Should be all good in 0.4.1.

jchapuis commented 3 years ago

awesome thanks!