EIP:
We will have a number of new datatypes with coercion functions like
Id
,unId
,Const
andunConst
. To reduce clutter, we introduce a common notation for such coercions.
In Scala, implicits and type inference take us pretty far. But by dealing with typeclass a lot, we also end up seeing some of the weaknesses of Scala’s type inference. One of the issues we come across frequently is its inability to infer partially applied parameterized type, also known as SI-2712. If you’re reading this, please jump to the page, upvote the bug, or help solving the problem.
To work around this issue, Cats uses a typeclass called Unapply
:
/**
* A typeclass that is used to help guide scala's type inference to
* find typeclass instances for types which have shapes which differ
* from what their typeclasses are looking for.
*
* For example, [[Functor]] is defined for types in the shape
* F[_]. Scala has no problem finding instance of Functor which match
* this shape, such as Functor[Option], Functor[List], etc. There is
* also a functor defined for some types which have the Shape F[_,_]
* when one of the two 'holes' is fixed. For example. there is a
* Functor for Map[A,?] for any A, and for Either[A,?] for any A,
* however the scala compiler will not find them without some coercing.
*/
trait Unapply[TC[_[_]], MA] {
// a type constructor which is properly kinded for the typeclass
type M[_]
// the type applied to the type constructor to make an MA
type A
// the actual typeclass instance found
def TC: TC[M]
// a function which will coerce the MA value into one of type M[A]
// this will end up being the identity function, but we can't supply
// it until we have proven that MA and M[A] are the same type
def subst: MA => M[A]
}
I think it’s easier to demonstrate this using an example.
scala> import cats._, cats.data._, cats.implicits._
import cats._
import cats.data._
import cats.implicits._
scala> def foo[F[_]: Applicative](fa: F[Int]): F[Int] = fa
foo: [F[_]](fa: F[Int])(implicit evidence$1: cats.Applicative[F])F[Int]
In the above, foo
is a simple function that returns the passed in value fa: F[Int]
where F
forms an Applicative
.
Since Either[String, Int]
is an applicative, it should qualify.
scala> foo(Right(1): Either[String, Int])
<console>:22: error: no type parameters for method foo: (fa: F[Int])(implicit evidence$1: cats.Applicative[F])F[Int] exist so that it can be applied to arguments (Either[String,Int])
--- because ---
argument expression's type is not compatible with formal parameter type;
found : Either[String,Int]
required: ?F[Int]
foo(Right(1): Either[String, Int])
^
<console>:22: error: type mismatch;
found : Either[String,Int]
required: F[Int]
foo(Right(1): Either[String, Int])
^
<console>:22: error: could not find implicit value for evidence parameter of type cats.Applicative[F]
foo(Right(1): Either[String, Int])
^
We got the error “argument expression’s type is not compatible with formal parameter type.”
We can make an Unapply
version of foo
as follows:
scala> def fooU[FA](fa: FA)(implicit U: Unapply[Applicative, FA]): U.M[U.A] =
U.subst(fa)
fooU: [FA](fa: FA)(implicit U: cats.Unapply[cats.Applicative,FA])U.M[U.A]
Now let’s try passing in exactly same parameter as we tried:
scala> fooU(Right(1): Either[String, Int])
res1: scala.util.Either[String,Int] = Right(1)
It works. Let’s look into how this is implemented.
For Either
, a monad is formed for Either[AA, ?]
, which means
during map
the right side of the parameter might change like
List[Int]
changing to List[String]
, but the left side stays put.
sealed abstract class Unapply2Instances extends Unapply3Instances {
type Aux2Right[TC[_[_]], MA, F[_,_], AA, B] = Unapply[TC, MA] {
type M[X] = F[AA,X]
type A = B
}
implicit def unapply2right[TC[_[_]], F[_,_], AA, B](implicit tc: TC[F[AA,?]]): Aux2Right[TC,F[AA,B], F, AA, B] = new Unapply[TC, F[AA,B]] {
type M[X] = F[AA, X]
type A = B
def TC: TC[F[AA, ?]] = tc
def subst: F[AA, B] => M[A] = identity
}
....
}
First Cats is defining Aux2Right
as a type alias that defines the abstract types M[_]
and A
.
Next, it defines an implicit converter from an arbitray typeclass instance TC[F[AA,?]]
to Aux2Right[TC,F[AA,B], F, AA, B]
.
One area where Unapply
is used in Cats is where infix operators, also known as “syntax”,
is injected. These implicit converters are called “bedazzlers” in cecc3a.
package cats
package syntax
trait FunctorSyntax1 {
implicit def functorSyntaxU[FA](fa: FA)(implicit U: Unapply[Functor,FA]): Functor.Ops[U.M, U.A] =
new Functor.Ops[U.M, U.A]{
val self = U.subst(fa)
val typeClassInstance = U.TC
}
}
trait FunctorSyntax extends Functor.ToFunctorOps with FunctorSyntax1
Let’s try using *>
operator from Apply
:
scala> (Right(1): Either[String, Int]) *> Right(2)
res2: scala.util.Either[String,Int] = Right(2)
This works, likely thanks to the Unapply
.
AppFunc we looked at on day 11 lets us create ever more complex
composition of applicative functor instances.
It would be half as useful without the help of Unapply
, since part of the benefit
is that it can derive the instances automatically.
At the same time, Unapply
cannot possibly be the ultimate solution because
it requires all shapes to be spelled out up front, and the most complex type it handles currently is F[AA, B, ?]
.
I could go beyond this by making a product of compositions of monad instances with two parameters.
I can only guess that this type inference problem is a problem that needs to be solved by the Scala compiler itself by treating sequential composition and parallel composition (product) as a first-class constuct in the type system.