9 tips for using the Cats library in Scala

Original author: Mikołaj Koziarkiewicz
  • Transfer
Functional programming in Scala can be difficult to master due to some syntactic and semantic features of the language. In particular, some of the language tools and ways to implement what you have planned with the help of the main libraries seem obvious when you are familiar with them - but at the very beginning of studying, especially on your own, it is not so easy to recognize them.

For this reason, I decided it would be useful to share some functional programming tips in Scala. Examples and names correspond to cats, but the syntax in scalaz should be similar due to the general theoretical basis.

9) Extension method constructors

Let's start with, perhaps, the most basic tool - extension methods of any type that turn an instance into Option, Either, etc., in particular:

  • .someand the corresponding constructor method nonefor Option;
  • .asRight, .asLeftfor Either;
  • .valid, .invalid, .validNel, .invalidNelForValidated

Two main advantages of their use:

  1. It is more compact and understandable (since the sequence of method calls is saved).
  2. Unlike constructor options, the return types of these methods are extended to a supertype, i.e.:

import cats.implicits._

Although type inference has improved over the years, and the number of possible situations in which this behavior helps the programmer to stay calm has decreased, compilation errors due to overly specialized typing are still possible in Scala today. Quite often, the desire to bang your head against a table arises when working with Either(see chapter 4.4.2 of the book Scala with Cats ).

Something else on the topic: y .asRightand .asLeftstill one type parameter. For example, "1".asRight[Int]this Either[Int, String]. If this parameter is not provided, the compiler will try to output it and receive it Nothing. Nevertheless, it is more convenient than providing both parameters each time or not providing either, as in the case of constructors.

8) Fifty shades *>

The * * operator defined in any method Apply(that is, in Applicative, Monadetc.) simply means “process the initial calculation and replace the result with what is specified in the second argument”. In the language of the code (in case Monad):

fa.flatMap(_ => fb)

Why use an obscure symbolic operator for an operation that does not have a noticeable effect? Starting to use ApplicativeError and / or MonadError, you will find that the operation retains the error effect for the entire workflow. Take as an example Either:

import cats.implicits._
val success1 = "a".asRight[Int]
val success2 = "b".asRight[Int]
val failure = 400.asLeft[String]
success1 *> success2
success2 *> success1
success1 *> failure
failure *> success1

As you can see, even in the event of an error, the calculation remains short-circuited. *> it will help you in working with deferred calculations in tasks Monix, IOand the like.

There is a symmetric operation, <*. So, in the case of the previous example:

success1 <* success2

Finally, if the use of symbols is alien to you, it is not necessary to resort to it. *> Is just an alias productR, and * <is an alias productL.


In a personal conversation, Adam Warski (thanks, Adam!) Rightly remarked that in addition to *> ( productR), there is also >> from FlatMapSyntax. >> is defined in the same way as fa.flatMap(_ => fb), but with two nuances:

  • it is determined independently of productR, and therefore, if for some reason the contract of this method changes (theoretically, it can be changed without violating monadic laws, but I'm not sure about MonadError), you will not suffer;
  • more importantly, >> has a second operand called by call-by-name, i.e. fb: => F[B]. The difference in semantics becomes fundamental if you perform calculations that can lead to a stack explosion.

Based on this, I started using *> more often. One way or another, do not forget about the factors listed above.

7) Raise the sails!

Many take time to put the concept into their heads lift. But when you succeed, you will find that he is everywhere.

Like many terms soaring in the air of functional programming, it liftcame from category theory . I’ll try to explain: take an operation, change the signature of its type so that it becomes directly related to the abstract type F.

In Cats, the simplest example is Functor :

def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f)

This means: change this function so that it acts on a given type of functor F.

The lift function is often synonymous with "nested constructors" for a given type. So, EitherT.liftFessentially an EitherT.right.example from Scaladoc is :

import cats.data.EitherT
import cats.implicits._

Cherry on the cake: liftpresent in the Scala standard library everywhere. The most popular (and perhaps the most useful in everyday work) example is PartialFunction:

val intMatcher: PartialFunction[Int, String] = {
    case 1 => "jak się masz!"
val liftedIntMatcher: Int => Option[String] = intMatcher.lift
//Some(jak się masz!)
//jak się masz!
//Exception in thread "main" scala.MatchError: 0

Now we can move on to more pressing issues.

6) mapN

mapN- A useful helper function for working with tuples. Again, this is not a novelty, but a replacement for the good old operator |@|, aka “Scream”.

Here's what mapN looks like in the case of a tuple of two elements:

// where t2: Tuple2[F[A0], F[A1]]
def mapN[Z](f: (A0, A1) => Z)(implicit functor: Functor[F], 
                              semigroupal: Semigroupal[F]): F[Z] = 
                                            Semigroupal.map2(t2._1, t2._2)(f)

In essence, it allows us to map values ​​inside a tuple from any F that are a semigroup (product) and a functor (map). So:

import cats.implicits._
("a".some, "b".some).mapN(_ ++ _)
(List(1, 2), List(3, 4), List(0, 2).mapN(_ * _ * _))
//List(0, 6, 0, 8, 0, 12, 0, 16)

By the way, do not forget that with cats you get a map leftmapfor tuples too:

("a".some, List("b","c").mapN(_ ++ _))
//won't compile, because outer type is not the same
("a".some, List("b", "c")).leftMap(_.toList).mapN(_ ++ _)
//List(ab, ac)

Another useful feature .mapN is instantiating case classes:

case class Mead(name: String, honeyRatio: Double, agingYears: Double)
("półtorak".some, 0.5.some, 3d.some).mapN(Mead)

Of course, you would rather use a loop operator for this for, but mapN avoids monadic transformers in simple cases.

import cats.effect.IO
import cats.implicits._
//interchangable with e.g. Monix's Task
type Query[T] = IO[Option[T]]
def defineMead(qName: Query[String],
               qHoneyRatio: Query[Double],
               qAgingYears: Query[Double]): Query[Mead] =
  (for {
    name       <- OptionT(qName)
    honeyRatio <- OptionT(qHoneyRatio)
    agingYears <- OptionT(qAgingYears)
  } yield Mead(name, honeyRatio, agingYears)).value
def defineMead2(qName: Query[String],
                qHoneyRatio: Query[Double],
                qAgingYears: Query[Double]): Query[Mead] =
  for {
    name       <- qName
    honeyRatio <- qHoneyRatio
    agingYears <- qAgingYears
  } yield (name, honeyRatio, agingYears).mapN(Mead)

Methods have similar results, but the latter dispenses with monadic transformers.

5) Nested

Nested- in fact, a generalizing double of monad transformers. As the name suggests, it allows you to perform attachment operations under certain conditions. Here is an example for.map(_.map( :

import cats.implicits._
import cats.data.Nested
val someValue: Option[Either[Int, String]] = "a".asRight.some
Nested(someValue).map(_ * 3).value

In addition Functor, Nested generalizes operations Applicative, ApplicativeError and Traverse. Additional information and examples are here .

4) .recover / .recoverWith / .handleError / .handleErrorWith / .valueOr

Functional programming in Scala has a lot to do with handling the error effect. In ApplicativeErrorand MonadErrorthere are several useful techniques, and you can be useful to know the subtle differences between the main four. So, withApplicativeError F[A]:

  • handleErrorconverts all errors at the dial peer to A according to the specified function.
  • recover It acts in a similar way, but accepts partial functions, and therefore can convert errors selected by you into A.
  • handleErrorWithsimilar to handleError, but its result should look like F[A], which means it helps you convert errors.
  • recoverWithacts as recover, but also requires F[A]as a result.

As you can see, we can limit ourselves handleErrorWith and recoverWiththat cover all possible functions. However, each method has its advantages and is convenient in its own way.

In general, I advise you to familiarize yourself with the ApplicativeError API , which is one of the richest in Cats and inherited in MonadError - which means it is supported in cats.effect.IO, monix.Tasketc.

There is another method for Either/EitherT, Validatedand Ior- .valueOr. In fact, it works as .getOrElsefor Option, but is generalized for classes containing something “on the left”.

import cats.implicits._
val failure = 400.asLeft[String]
failure.valueOr(code => s"Got error code $code")
//"Got error code 400"

3) alley-cats

alley-cats is a convenient solution for two cases:

  • instances of tile classes that do not follow their laws 100%;
  • Unusual auxiliary typklassy, ​​which can be used properly.

Historically, the monad instance is the most popular in this project Try, because Try, as you know, it does not satisfy all monadic laws in terms of fatal errors. Now he is truly introduced to Cats.

Despite this, I recommend that you familiarize yourself with this module , it may seem useful to you.

2) Responsibly treat imports

You must know - from the documentation, books, or from somewhere else - that cats use a specific import hierarchy:

cats.xfor basic (kernel) types;
cats.datafor data types like Validated, monad transformers, etc .;
cats.syntax.x._ to support extension methods so that you can call sth.asRight, sth.pure, etc .;
cats.instances.x._ to directly import the implementation of various typclasses into the implicit scope for individual specific types, so that when calling, for example, sth.pure, the "implicit not found" error does not occur.

Of course, you notice an import cats.implicits._where all the syntax and all instances of the type class are imported in implicit scope.

In principle, when developing with Cats, you should start with a certain sequence of imports from the FAQ, namely:

import cats._
import cats.data._
import cats.implicits._

If you get to know the library better, you can combine it to your taste. Follow a simple rule:

  • cats.syntax.x provides extension syntax related to x;
  • cats.instances.x provides instance classes.

For example, if you need .asRightwhich is an extension method for Either, do the following:

import cats.syntax.either._
//Right[Int, String](a)

On the other hand, to get Option.pureyou must import cats.syntax.monadANDcats.instances.option :

import cats.syntax.applicative._
import cats.instances.option._

By manually optimizing your import, you will limit implicit scopes in your Scala files and thereby reduce compilation time.

However, please: do not do this if the following conditions are not met:

  • you have already mastered Cats well
  • your team owns the library at the same level

Why? Because:

//мы не помним, где находится `pure`,
//и стараемся быть умными
import cats.implicits._
import cats.instances.option._
//could not find implicit value for parameter F: cats.Applicative[Option]

This is because both cats.implicits, and cats.instances.optionare extensions cats.instances.OptionInstances. In fact, we import its implicit scope twice, than we confuse the compiler.

Moreover, there is no magic in the hierarchy of implicits - this is a clear sequence of type extensions. You just need to refer to the definition cats.implicitsand examine the type hierarchy.

For some 10-20 minutes you can study it enough to avoid problems like these - believe me, this investment will definitely pay off.

1) Do not forget about cats updates!

You may think that your FP-library of timeless, but really cats and scalaz actively updated. Take cats as an example. Here are just the latest changes:

Therefore, when working with projects, do not forget to check the library version, read the notes for new versions and update in time.

Also popular now: