Search code examples
scalascala-catscirce

Type resolution with flatMap


why do i need to add the type annotation at the first line? c.get[List[String]]("primary-group") is Decoder.Result[List[String]] after flatMap it should keep the top type and be Decoder.Result[String] but it changes to Either[DecodingFailure, String]. Why? Is the problem that it is dependent type?

  case class JWTPayload(primaryGroup: Group, groupMember: List[Group], name: String, pid: String)

  implicit val jwtPayloadDecoder: Decoder[JWTPayload] = Decoder.instance(c =>
    (
      c.get[List[String]]("primary-group").flatMap(l => if(l.size == 1) l.head.asRight else DecodingFailure("", c.history).asLeft) : Decoder.Result[String],
      c.get[List[String]]("group-member"),
      c.get[String]("name"),
      c.get[String]("pid")
    ).map4(
     JWTPayload
    )
  )

Without : Decoder.Result[String I get

Error:(43, 7) value map4 is not a member of (scala.util.Either[io.circe.DecodingFailure,String], io.circe.Decoder.Result[List[String]], io.circe.Decoder.Result[String], io.circe.Decoder.Result[String])
possible cause: maybe a semicolon is missing before `value map4'?
    ).map4(

Thanks


Solution

  • This is not a full answer but I hope it will provide some insights. The crucial part here is how map4 is implemented. As of cats 0.9 it is done via cats.syntax.TupleCartesianSyntax trait and its implicit catsSyntaxTuple4Cartesian which wraps a 4-tuple into a cats.syntax.Tuple4CartesianOps class (in cats 1.0 "cartesian" was changed to "semigroupal"). This code is auto-generated for all tuples up to 22 by Boilerplate.scala. The auto-generated code looks something like this:

    implicit def catsSyntaxTuple4Cartesian[F[_], A0, A1, A2, A3](t4: Tuple4[F[A0], F[A1], F[A2], F[A3]]): Tuple4CartesianOps[F, A0, A1, A2, A3] = new Tuple4CartesianOps(t4)
    
    
    private[syntax] final class Tuple4CartesianOps[F[_], A0, A1, A2, A3](t4: Tuple4[F[A0], F[A1], F[A2], F[A3]]) {
      def map4[Z](f: (A0, A1, A2, A3) => Z)(implicit functor: Functor[F], cartesian: Cartesian[F]): F[Z] = Cartesian.map4(t4._1, t4._2, t4._3, t4._4)(f)
      def contramap4[Z](f: Z => (A0, A1, A2, A3))(implicit contravariant: Contravariant[F], cartesian: Cartesian[F]): F[Z] = Cartesian.contramap4(t4._1, t4._2, t4._3, t4._4)(f)
      def imap4[Z](f: (A0, A1, A2, A3) => Z)(g: Z => (A0, A1, A2, A3))(implicit invariant: Invariant[F], cartesian: Cartesian[F]): F[Z] = Cartesian.imap4(t4._1, t4._2, t4._3, t4._4)(f)(g)
      def apWith[Z](f: F[(A0, A1, A2, A3) => Z])(implicit apply: Apply[F]): F[Z] = apply.ap4(f)(t4._1, t4._2, t4._3, t4._4)
    }
    

    Note the F[_] (functor) type parameter. Effectively this code adds map4 method to any 4-tuple where each inner type is the same functor over some types.

    So assuming you did import cats.implicits._, after (partial) implicits resolution your code is actually something like this:

    cats.implicits.catsSyntaxTuple4Cartesian[Decoder.Result, String, List[String], String, String](
      c.get[List[String]]("primary-group").flatMap(l => if (l.size == 1) l.head.asRight else DecodingFailure("", c.history).asLeft): Decoder.Result[String],
      c.get[List[String]]("group-member"),
      c.get[String]("name"),
      c.get[String]("pid")
    ).map4[JWTPayload](
      JWTPayload
    )
    

    When you don't specify Decoder.Result[String], Scala compiler is not smart enough to get that it should split Either[DecodingFailure, String] into a functor type Either[DecodingFailure, _] and String and then

    1. there will be matching Functor and Cartesian implicit objects (actually provided by the cats.implicits object via cats.instances.AllInstances and cats.instances.EitherInstances traits)

    2. it would match the functor type used for other 3 fields in the tuple (i.e. Decoder.Result[_]).

    So I think this behavior is a result of a combination of the fact that map4 is added to 4-tuple via an implicit Ops-class and the fact that the underlying type is Either which is 2-places generic type rather than a simple Functor.