📜 ⬆️ ⬇️

9 tips on using the Cats library in Scala

Functional programming in Scala can be hard to master due to some syntactic and semantic features of the language. In particular, some of the language tools and ways of implementing what was planned with the help of the main libraries seem obvious when you are familiar with them - but at the very beginning of the study, especially self-learning, it is not so easy to recognize them.

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



9) Constructors of extension methods


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

Two main advantages of using them:

  1. So compact and clearer (since the sequence of method calls is preserved).
  2. Unlike constructor variants, the return types of these methods are expanded to a supertype, that is:

 import cats.implicits._ Some("a") //Some[String] "a".some //Option[String] 

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

Something else on the topic: .asRight and .asLeft still have one type parameter. For example, "1".asRight[Int] is Either[Int, String] . If you do not provide this parameter, the compiler will try to output it and get Nothing . Still, it is more convenient than either to provide both parameters each time or not to provide either one, as in the case of constructors.

8) Fifty shades *>


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

 fa.flatMap(_ => fb) 

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

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

As you can see, even in case of an error, the calculation remains short-circuited. *> will help you in working with deferred calculations in Monix , IO and other similar Monix .

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

 success1 <* success2 //Right(a) 

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

Note


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


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

7) Raise the sail!


It takes time for many to lay down the concept of lift . But when you succeed, you will find that it is everywhere.

Like many terms hovering in the air of functional programming, lift comes from category theory . I will 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.liftF is essentially EitherT.right. Scaladoc example :

 import cats.data.EitherT import cats.implicits._ EitherT.liftF("a".some) //EitherT(Some(Right(a))) EitherT.liftF(none[String]) //EitherT(None) 

Cherry on the cake: lift present in the standard Scala library everywhere. The most popular (and, perhaps, the most useful in daily work) example is PartialFunction :

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

Now you can go to more pressing issues.

6) mapN


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

Here is what mapN looks like in the case of a two-element tuple:

 // 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 the 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(_ ++ _) //Some(ab) (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 and leftmap for tuples:

 ("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 of .mapN is instantiating case classes:

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

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

 import cats.effect.IO import cats.implicits._ //interchangable with eg 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 a similar result, but the latter goes without monadic transformers.

5) Nested


Nested - in fact, a generalizing twin monadny transformers. As the name suggests, it allows you to perform attachments 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 //Some(Right(aaa)) 

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

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


Functional programming in Scala is largely due to the processing of the effect of an error. ApplicativeError and MonadError have several useful methods, and it may be useful for you to learn the subtle differences between the main four. So, with ApplicativeError F[A]:


As you can see, you can restrict handleErrorWith and recoverWith , which 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 from MonadError - and therefore supported in cats.effect.IO , monix.Task , etc.

There is another method for Either/EitherT , Validated and Ior - .valueOr . In fact, it works like .getOrElse for Option , but it is generalized for classes that contain something “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:


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

Despite this, I recommend reading this module , it may seem useful to you.

2) Responsible to import


You must know — from the documentation, the book, or from somewhere else — that cats uses a particular import hierarchy:

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

Of course, you noticed cats.implicits._ import, which imports all the syntax and all instances of the type class into the 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._ 

Acquainted with the library closer, you can combine to your taste. Follow the simple rule:


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

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

On the other hand, to get Option.pure you must import cats.syntax.monad And cats.instances.option :

 import cats.syntax.applicative._ import cats.instances.option._ "a".pure[Option] //Some(a) 

Thanks to manual optimization of your import, you limit implicit scopes in your Scala files and thereby shorten the compilation time.

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


Why? Because:

 //  ,   `pure`, //    import cats.implicits._ import cats.instances.option._ "a".pure[Option] //could not find implicit value for parameter F: cats.Applicative[Option] 

This happens because cats.implicits and cats.instances.option are extensions of cats.instances.OptionInstances . In fact, we import its implicit scope twice, which confuses the compiler.

At the same time, there is no magic in the hierarchy of implications - this is a clear sequence of type extensions. You only need to refer to the definition of cats.implicits and explore 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 the cats update!


You may think that your FP library is timeless, but in fact 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 version of the library, read the notes to the new versions and be updated in time.

Source: https://habr.com/ru/post/448128/


All Articles