The Scala's cats library has a bunch of operators. For this reason I decided to create a blog post that could act as quick reference for learning how to use all of them. This is inspired in the Cats Docs, but I added an explanation to each operator.

Also, you will find that I started describing some annotations and then some symbology used in the documentation or comments. I think this is useful because I was unsure of their meaning the first time I saw them.

@specialized, @sp

@speciallized(@op for short) prevents the boxing of java primitive types and therefore the unnecessary instantiation of object in the heap. Even if scala has an unified typed system we need to manually use @sp because the optimization can produce larger compilation times and bigger artifacts.

@typeclass, @op: simulacrum annotations

The Simulacrum library provides @typeclass and @op to help us to define type-classes and operators.

<->, <=>: Equivalent

If you see the <-> or <=> symbols in the comments, these likely represent "equivalency":

  • <->: $$ \longleftrightarrow $$
  • <=>: $$ \Longleftrightarrow $$

And please don't get confused with the pminus of Spire, that is unrelated.

For example, when you see: IO.suspend(f) <-> IO(f).flatten what it really means is that IO.suspend(f) is equivalent($$ \longleftrightarrow $$) to IO(f).flatten.

>, >=, <, <=: PartialOrder[A] operators

The normal and expected order comparison operators:

  • >: gt(x: A, y: A): Boolean
  • >=: gteqv(x: A, y: A): Boolean
  • <: lt(x: A, y: A): Boolean
  • <=: lteqv(x: A, y: A): Boolean

We can use ParialOrder[A] to compare the malevolence of our cats:

case class BadCat(kills: BigInt)
implicit val badCatPO = new PartialOrder[BadCat] {
  override def partialCompare(x: BadCat, y: BadCat): Double =
    implicitly[PartialOrder[BigInt]].partialCompare(x.kills,y.kills)
}

*>, <*, <*>: Apply[F[_]] operators

Apply in an Applicative without pure. It has 3 operators:

trait Apply[F[_]] {
  // <*> applies the function to the value.
  def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]
  // *> Compose two actions, discarding any value produced by the first.
  def productR[A, B](fa: F[A])(fb: F[B]): F[B] = ???
  // <* Compose two actions, discarding any value produced by the second.
  def productL[A, B](fa: F[A])(fb: F[B]): F[A] = ???
}

Here is an example of how we can apply a F[A=>B] to a F[A] and also discard values:

val multiply: Option[Int => Int] = Some{ _ * 2}
multiply <*> Some(4) /* Option[Int] = Some(8) */
1.some *> 2.some /* Option[Int] = Some(2) */
1.some <* 2.some /* Option[Int] = Some(1) */

Why would you want to discard your results? Maybe you don't care about the result but you do about the sequencing:

val p = IO(println("hi")) *> IO(println("bye"))
p.unsafeRunSync()
/* hi
/* bye

<&, &>: Parallel[M[_], F[_]] operators

A type that can represent a parallel relationship from a monad M[_] and an applicative F[_].

&>, is like *> but for parallel

parProductR[A, B](ma: M[A])(mb: M[B]): M[B]

<&, is like <* but for parallel

parProductL[A, B](ma: M[A])(mb: M[B]): M[A]

===, =!=: Eq[A] operators

public boolean equals(Object anObject) 

In Java we compare two object with equals, it's basically a non-typed comparison. Eq[A] gives us type safety for comparisons back.

trait Eq[@sp A] extends Any {
  // ===
  def eqv(x: A, y: A): Boolean
  // =!=
  def neqv(x: A, y: A): Boolean = !eqv(x, y)
}

We use Eq for typed comparisons:

case class MutantDog(eyes: BigInt)
implicit val mutantEq = new Eq[MutantDog]{
  override def eqv(x: MutantDog, y: MutantDog): Boolean =
    x.eyes === y.eyes
}
MutantDog(1) === MutantDog(1) /* true */
MutantDog(1) === MutantDog(2) /* false */
MutantDog(1) == true /* false, but you shouldn't be able to do that */
MutantDog(1) === true /* doesn't compile */

>>=, >>: FlatMap[F[_]] operators

The first operator >>= is the flatMap. The following example is pretty much like haskell's >>= bind operator and do-notation

(2.some >>= { x =>
  Some(x * 2)
} >>= { x =>
  Some(x + 1)
}) === (for {
  a <- 2.some
  b <- Some(a * 2)
  c <- Some(b + 1)
} yield c) /* true */

>> is the followed by operator, it has the following signature:

def >>[B](fb: => F[B])(implicit F: FlatMap[F]): F[B]

This does the same that *> of Apply[F[_]] does, but fb is a by-name argument. You use this when you need to evaluate fb inside flatMap to ensure stack-safety.

|-|,|+|, <+>

Group[A] is a Monoid[A] where every element has an opposite. Semigroup[A] wraps any A with the associative operation combine. SemigroupK[F[_]] is like Semigroup[A] but operates with type constructors F[_].

  • |-| is the remove(x: A, y: A): A for Group[A]
  • |+| is the combine(x: A, y: A): A for Semigroup[A]
  • <+> is the combineK(x: F[A], y: F[A]): F[A] for Semigroup[F[_]]

<<<, >>>: Compose[F[_,_]] operators

>>> is the normal andThen operator, it has the same logic than normal function composition g(f(x)) but is more general, you can use it for F[_,_]s:

val map1 = Map( 1 -> "a", 2 -> "b")
val map2 = Map("a" -> true, "b" -> false)
val map3 = map1 >>> map2
map1(1) /* true */
map2(2) /* true */

finally <<< is just like >>>: f <<< g $$ \longleftrightarrow $$ f >>> g

***, &&&: Arrow[F[_,_]] operators

When we talk about arrows the first operator we think about are the >>> and <<< but they are defined for Compose[F[_,_]] right? Well...

  • Compose[F[_,_]] brings >>> and <<<
  • Category[F[_,_]] extends Compose[F[_,_]]
  • Arrow[F[_,_]] extends Category[F[_,_]] therefore every arrow supports >>> and <<<.

But arrow gives us more:

***

split[A, B, C, D](f: F[A, B], g: F[C, D]): F[(A, C), (B, D)]

&&&

merge[A, B, C](f: F[A, B], g: F[A, C]): F[A, (B, C)]

Type aliases

type ⊥ = Nothing
type ⊤ = Any
type ~>[F[_], G[_]] = arrow.FunctionK[F, G]
type :<:[F[_], G[_]] = InjectK[F, G]