Skip to content

Instantly share code, notes, and snippets.

@sellout
Last active March 28, 2017 18:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sellout/775f32436a5c9fc7bf66a0d2570a31bd to your computer and use it in GitHub Desktop.
Save sellout/775f32436a5c9fc7bf66a0d2570a31bd to your computer and use it in GitHub Desktop.

Porting from Scalaz to Cats (and friends)

Mappings

There are a lot of cases where things simply differ by name. These are the easiest changes to make.

  • scalaz.Zip -> cats.Cartesian (with the primary operation zip -> product)
  • scalaz.Unzip -> cats.MonadCombine ... sort of
  • scalaz.Task -> fs2.Task (or monix.Task)

Additionally, some Scalaz data types are gone, in favor of using the original Scala types. E.g.

  • \/ -> Either (and -\/ -> Left, \/- -> Right)
  • IList -> List
  • IMap -> Map
  • ISet -> Set
  • Maybe -> Option

More complicated differences

Eval

The use of a: => A for laziness has been replaced with a: cats.Eval[A], which is overall much more flexible, but puts a bit more work on the caller. E.g., what was _.foldRight(1)(_ + _) in Scalaz looks like _.foldRight(now(1))((e, a) => a.map(e + _)).value in Cats.

Traverse

  • missing default implementations for foldLeft and foldRight (for efficiency reasons)

Newtypes

Scalaz uses type tags like Int @@ Max to select specific instances when multiple are available. This is handled differently in the Typelevel world. The Newts library provides wrappers like Max[A] for the same purpose, with a Newtype case class that provides wrap and unwrap methods.

The unwrap method has the same name in Cats and Scalaz, but the wrapping is different (e.g., Scalaz’s .disjunction vs Newts’ .asAny).

Techniques

Supporting both

Shims

This is a nice option when your usage of Scalaz falls into its patterns -- basically, only relying on type classes and potentially providing instances for your type classes on Scalaz types. This allows you to abstract over the type classes, and then push the instances down into mylibrary-scalaz and mylibrary-cats subprojects.

This is probably the easiest to manage if it fits you usage.

Yax

Yax is like a Scala preprocessor. It allows you to indicate which blocks of code should be used with which library.

To make it less intrusive, I recommend renaming imports as possible:

#+scalaz
import scalaz._
#-scalaz
#+cats
import cats._
import cats.kernel.{Eq => Equal}
import scala.{Either => \/}
#-cats

This reduces the amount of changes that need to happen in the code. Additionally, choose shared method names if possible. For example, Scalaz calls monadic η point, but Cats calls it pure. However, Scalaz also provides pure as an alias, so if you use that name consistently, you can avoid conditionalizing those methods.

A list:

  • Applicative.pure instead of .point
  • === instead of

Branches

This is useful if you transitioning from one to the other, and likely only have a fairly short period of supporting both.

The hardest part here is keeping both branches in sync with new changes. It's helpful to minimize the changes so that merging between them has fewer (and more obviously managed) conflicts.

Similar to the Yax approach, you can largely do import renaming then restrict in-code changes to different method names.

In general, as long as you're supporting both, you want to avoid making too many stylistic changes that don't map well between the two libraries.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment