Typelevel has just published Cats 2.0.0, and while the core modules are guaranteed to be binary compatible with 1.x, there are some changes that break source compatibility. Most of these changes are unlikely to affect users, but a few will, and the goal of this post is to point out which those are and what you can do about them.

Note that while some of the stuff below is pretty intense, it's unlikely to apply to you. In fact if you're not using Parallel, there's like a 99% chance you can close this tab right now and go change your Cats version and everything will be fine. There are also always people in the Cats Gitter channel who are happy to help. In any case please don't be intimidated and put off updating to 2.0.0β€”the community is healthier if adopters invest in staying up to date.

TestingπŸ”—

If you depend on cats-laws or cats-testkit, you'll definitely need to be sure that you're using ScalaCheck 1.14 instead of 1.13, which the 1.x versions of these modules depended on. You'll also need to be sure that your dependencies that transitively bring in either these modules or ScalaCheck have been updated.

Note that the optional ScalaCheck dependency in ScalaTest changed from 1.13 to 1.14 between ScalaTest 3.0.5 and 3.0.6, so to be absolutely safe you should update to a version of ScalaTest that's more recent than 3.0.5, but in practice I don't remember whether that's likely to cause problems.

It's likely that you'll also need to make some changes to any laws-checking code you have, since for example both Discipline and cats-testkit have dropped their ScalaTest dependencies since 1.x. The Cats documentation on laws checking is up to date for 2.0.0 and fairly comprehensive.

DependenciesπŸ”—

In general you don't need to worry about whether all of your non-test dependencies that transitively depend on Cats have been updated to 2.0.0 yet, as long as they're available for at least 1.x. Most other libraries are unlikely to depend on the non-core Cats modules that do break binary compatibility (cats-laws, cats-kernel-laws, cats-testkit, and alleycats), and since the core module jars are a drop-in replacement for the 1.x jars at runtime, you shouldn't need to worry about any cats-core evictions you see in your build. You can also be confident that everything will definitely be fine (at least in this respect) if your code compiles.

There are exceptions that can lead to your code not compiling because your dependencies haven't been updated, though. If you update your cats-core dependency to 2.0.0 but leave cats-effect at 1.4.0, for example, the last two lines in the following code won't compile:

import cats.Parallel, cats.effect.{ContextShift, IO}
import scala.concurrent.ExecutionContext

implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global)

Parallel[IO]
Parallel[IO, IO.Par]

To make matters worse, the compiler's errors will be completely unhelpful:

scala> Parallel[IO]
<console>:16: error: could not find implicit value for parameter P: cats.Parallel[cats.effect.IO]
       Parallel[IO]
               ^

The compiler fails to find the instance here because cats-effect 1.4.0 provides the Parallel instance for IO using the older version of Parallel with two type parameters (see the next section for more details about this change). Even though you've updated your source to use the new version, and even though the new version is binary-compatible with the old version, the fact that you're on a version of cats-effect that uses the old version means you're out of luck, because the implicit resolution relies on source compatibility that we've broken.

Fortunately cats-effect has already been updated, and this issue is unlikely to come up in many other cases. If you do find yourself with disappearing instances like this, you can run sbt evicted to see if the dependencies that are providing the instances are still on Cats 1.x.

Using Parallel instancesπŸ”—

The biggest source-compatibility-breaking change in 2.0.0 was introduced in the second release candidate, and involves Parallel's second type parameter becoming a type member. I've previously written about how and why we made this change, so I'll just focus here on what you'll need to do about it.

If you're currently using cats-par, there's not much you have to do. You can remove your cats-par dependency (there is a release that should work with 2.0.0, but it's no longer necessary and everything in it has been deprecated), remove your cats.temp.par imports, and replace cats.temp.par.Par everywhere with cats.Parallel.

The rest of this section and the next will focus on the case where you're not using cats-par, or where you're mixing Par and Parallel in your code.

If you're referring to cats.Parallel explicitly, you'll generally be able to get away with just deleting the second type parameter. For example, if you have this method (borrowed from the cats-par documentation):

def withoutPar[F[_]: Monad, G[_], A, C, D](as: List[A], f: A => Kleisli[F, C, D])
  (implicit P: Parallel[F, G]): Kleisli[F, C, List[D]] =
    as.parTraverse(f)

You'd remove the G:

def withoutPar[F[_]: Monad, A, C, D](as: List[A], f: A => Kleisli[F, C, D])
  (implicit P: Parallel[F]): Kleisli[F, C, List[D]] =
    as.parTraverse(f)

And you're done. You can even make it a little nicer by making Parallel a constraint bound:

def withoutPar[F[_]: Monad: Parallel, A, C, D]
  (as: List[A], f: A => Kleisli[F, C, D]): Kleisli[F, C, List[D]] =
    as.parTraverse(f)

But that's just syntactic sugar.

In some (less common) cases, you may need to keep the type parameter around, but with some rearrangements. Suppose you have the following (which doesn't really make sense as something you'd actually want to do, but the fact that I have to stretch for an example is good, I guess):

import cats.{CommutativeApplicative, FlatMap, Parallel}

import cats.instances.list._, cats.syntax.parallel._

def parUSeqList[M[_], F[_], A](xs: List[M[List[A]]])
  (implicit P: Parallel[M, F], F: CommutativeApplicative[F]) = 
    xs.parUnorderedSequence

You can't just remove the F this time, because it has a constraint (CommutativeApplicative), so you'll have to add a .Aux:

import cats.{CommutativeApplicative, FlatMap, Parallel}

import cats.instances.list._, cats.syntax.parallel._

def parUSeqList[M[_], F[_], A](xs: List[M[List[A]]])
  (implicit P: Parallel.Aux[M, F], F: CommutativeApplicative[F]) = 
    xs.parUnorderedSequence

You might also run into a situation where the F appears in your return type, and the solution is the same: add .Aux. The next section goes into more detail about what this Aux thing does.

Writing Parallel instancesπŸ”—

If you're writing your own Parallel instances, the story is a little more complicated. For example, suppose you currently have an instance like this:

implicit val myTaskPar: Parallel[MyTask, MyTask.Par] = new Parallel[MyTask, MyTask.Par] {
  //...
}

This will no longer compile, because the second type parameter is gone. The following will work:

implicit val myTaskPar: Parallel.Aux[MyTask, MyTask.Par] = new Parallel[MyTask] {
  type F[x] = MyTask.Par[x]
  //...
}

Or, equivalently but much more verbosely, you can write out the refinement by hand:

implicit val myTaskPar: Parallel[MyTask] { type F[x] = MyTask.Par[x] } =
  new Parallel[MyTask] {
    type F[x] = MyTask.Par[x]
    //...
  }

There's really no good reason to do that, though.

Note that both the F type member and the Aux part (or the refinement) are necessary! Your instance will compile if you have the F but not the Aux:

implicit val myTaskPar: Parallel[MyTask] = new Parallel[MyTask] {
  type F[x] = MyTask.Par[x]
  //...
}

But this is likely to not do what you want at some point down the line. When you try to resolve a Parallel[MyTask] instance with this version, the compiler will give you back an unrefined Parallel, since by putting the Parallel[MyTask] annotation on it here you've explicitly asked the compiler to forget the F part.

The worst part here again is that the error messages you'll end up seeing because you forgot an Aux are likely to be totally unhelpful, so you really just have to remember never to write an implicit Parallel without the Aux.

You can read this post by Stephen Compall or this Stack Overflow answer for more details about what's going on here.

Parallel applyπŸ”—

There's also a new Parallel.apply, so you can now use it with either one or two type parameters:

scala> import cats.instances.parallel._, cats.instances.string._
import cats.instances.parallel._
import cats.instances.string._

scala> type E[x] = Either[String, x]
defined type alias E

scala> type V[x] = cats.data.Validated[String, x]
defined type alias V

scala> cats.Parallel[E, V]
res0: cats.Parallel.Aux[E,V] = cats.instances.ParallelInstances$$anon$1@112d5b55

scala> cats.Parallel[E]
res1: cats.Parallel[[x]scala.util.Either[String,x]]{type F[x] = cats.data.Validated[String,x]} = cats.instances.ParallelInstances$$anon$1@5d448f02

Note that in the second version we only asked for a Parallel[E], but we got back a refined type equivalent to Parallel.Aux[E, V]. This is intentional, and it's nice, because it means we can actually use methods like parallel that refer to the F in their return types. If we had used implicitly instead, we'd get something much less useful:

scala> val inst = implicitly[cats.Parallel[E]]
inst: cats.Parallel[E] = cats.instances.ParallelInstances$$anon$1@2bd44f11

scala> val s = inst.parallel(Left("foo"): Either[String, Int])
s: inst.F[Int] = Invalid(foo)

This inst.F[Int] is almost as bad as Any. We can't do anything with it except pass it back to other methods on inst. We specifically don't know that it's a Validated:

scala> s.toEither
<console>:19: error: value toEither is not a member of inst.F[Int]
       s.toEither
         ^

With Parallel.apply we don't have this problem:

scala> val inst = cats.Parallel[E]
inst: cats.Parallel[[x]scala.util.Either[String,x]]{type F[x] = cats.data.Validated[String,x]} = cats.instances.ParallelInstances$$anon$1@7dd2b7f2

scala> val s = inst.parallel(Left("foo"): Either[String, Int])
s: inst.F[Int] = Invalid(foo)

scala> s.toEither
res16: Either[String,Int] = Left(foo)

Note that there are some cases where the new apply can cause old code not to compile. For example, both of the following compile on Cats 1.x but not 2.0.0:

Parallel.apply: Parallel[Stream, ZipStream]

val p: Parallel[Stream, ZipStream] = Parallel.apply

The solution here is "don't do that". In general the apply on type classes isn't really intended for use with inferred type parameters, and in this case it's just not supported.