9 tips about using cats in Scala you might want to know
BuzzFeed may be laying people off, but we’re still doing listicles!
Edited 2019–02–07 19:00 UTC— added note to the *>
section.
Functional programming in Scala, due to various syntactic and semantic idiosyncrasies of the language, can be more difficult to get into than it otherwise should. Specifically, there are some features of, and “proper ways to do stuff”, within the core FP libraries that are obvious once you know of them — yet are not so trivial to discover when you’re starting up, especially without guidance.
So, I thought it helpful to share some usage tips for FP in Scala in this post. The examples and specific names are for cats, but scalaz syntax should be similar, owing to common theoretical background.
9) Extension method constructors
Let’s start with probably the most basic feature — extension methods for any type that convert an instance into an Option
, Either
etc., namely:
.some
, and the correspondingnone
constructor method forOption
.asRight
,.asLeft
forEither
,.valid
,.invalid
,.validNel
,.invalidNel
forValidated
The advantages of using them are twofold:
- It’s arguably more compact and understandable (since it’s in the same ordering as a method call chain).
- Unlike the constructor variants, these methods’ return types are widened to the supertype, i.e.:
While type inference has improved over the years, and the number of scenarios where this behavior is relevant to avoiding programmer frustration has decreased, compile errors due to over-specializing typing can still happen in modern Scala. Commonly, such “headdeskers” occur with Either
(see Chapter 4.4.2 of Scala with Cats for a demonstration).
One more related thing to remember: .asRight
and .asLeft
still have one type parameter. For example, "1".asRight[Int]
is Either[Int, String]
. If you do not provide that parameter, the compiler will try to infer it, and may come up with Nothing
. Still, this is more convenient than always providing none or both parameters, as with the constructors.
8) "*> Tales, woo-oo!"
The *>
operator, defined on any Apply
(so, Applicative
, Monad
etc.), simply means "process the original computation, and replace the result with whatever is given in the second argument", or, in code terms (in the Monad
variant) :
Why should you bother using an — arguably confusing — symbolic operator for a seemingly no-effect operation? Well, once you start using ApplicativeError
and/or MonadError
, you’ll find the operation preserves the error effect for your entire flow. Let’s use Either
as an example:
As you can see, if an error occurs, the computation is still short-circuited. Using *>
then becomes a frequently useful shortcut when dealing with delayed computation through Monix
task, IO
, or the like.
There’s also a symmetric operation, <*
. So, as with our previous setup:
Finally, if you’re not partial to using symbols, you don’t have to do that:
*>
is simply an alias forproductR
,- and
<*
forproductL
.
EDIT/NOTE
During a conversation with Adam about these operators (thanks Adam!), he rightly pointed out that apart from *>
/ productR
, there’s also >>
from FlatMapSyntax
. >>
is similarly defined as fa.flatMap(_ => fb)
, but with two differences:
- it’s defined independently from
productR
, so in case the contract of that method changes for some reason (theoretically it can be different without breaking monadic laws, but I’m not so sure about those ofMonadError
) , you’re safe; - more importantly,
>>
has the second operand be invoked call-by-name, i.e.fb: => F[B]
. The semantic difference may be crucial for you, if you’re running some potentially stack-exploding calculations.
With that in mind, I personally found *>
to be used more often (and also it afforded me the opportunity of an old cartoon reference in the heading). Nevertheless, keep the considerations outlined above in mind.
7) Thoust thou even lift?
The concept of lift is one of those things that takes you a while to intuitively comprehend, but once you do, you’ll see it everywhere.
Like many terms floating about the ether of functional programming, it’s a concept from category theory. The best way I can explain it is: given some operation, change its type signature in such a way so it is more directly “relatable” to some more abstract type F
.
In cats, the basic example is that in Functor
:
meaning modify the given function so that it now operates on the given functor type F
.
Lift function are often synonymous with “embedding constructors” for a given type, like with EitherT.liftF
, which is basically EitherT.right
. Based on the example from the Scaladoc:
As a cherry on top — lift
actually appears throughout the standard Scala library. The most prominent example (and probably the most day-to-day-useful one) is the one on PartialFunction
:
Now, let’s head on to something more immediately practical.
6) mapN
mapN
is a helpful utility function in the context of tuples. It is also not a new thing, but essentially a replacement of the good-old |@|
, i.e. "Scream" operator syntax.
Returning to the subject at hand, let’s see how mapN
looks like for a 2-element tuple:
Essentially, it allows you to map within a tuple of any F
s that are Semigroupal
s (product
) and Functor
s (map
). So:
By the way, remember that, with cats, you also get map
and leftMap
on tuples:
Yet another thing that .mapN
helps out with is instantiating case classes:
Usually, of course, you would use a for
loop for such things, but mapN
let’s you avoid monad transformers in the simple cases:
Both methods have the same result, while the latter does away with the need for monad transformers.
5) Nested
Nested
is basically a generalized counterpart of Monad Transformers. As the name suggests, it allows you to make nested operations, under some conditions. Here’s an example for .map(_.map(
:
Apart from Functor
, Nested
also generalizes operations from Applicative
, ApplicativeError
and Traverse
. For more information and examples, see here.
4) .recover
/.recoverWith
/.handleError
/.handleErrorWith
/.valueOr
A lot of FP-in-Scala programming rotates around handling error effects. There’s several useful methods in ApplicativeError
and MonadError
to help you, and it pays to know the subtle differences between the arguably four most basic ones. So, given some F[A]
that is an ApplicativeError
:
handleError
converts all errors at the invocation point intoA
according to the given function,recover
is similar, but accepts a partial function, so it can also convert selected errors toA
,handleErrorWith
is likehandleError
, but the result is supposed to beF[A]
, so it effectively also allows you to remap errors,recoverWith
is likerecover
, but also requiresF[A]
as the result.
As you can see, it might have been as well handleErrorWith
or recoverWith
, as either can fully express all the others. Nevertheless the entire set is useful for convenience reasons.
In general, I do recommend reviewing the API of ApplicativeError
, as it’s one of the most rich ones in cats, and is shared by MonadError
- hence supported by cats.effect.IO
, monix.Task
, and others.
Finally, there’s one more method supplied for Either
/EitherT
, Validated
and Ior
- .valueOr
. It’s essentially like .getOrElse
for Option
, but generalized for classes that have "something" on the "left" side.
3) alley-cats
alley-cats
is a pragmatic solution for handling the existence of two things:
- typeclass instances for types that don’t 100% follow their laws,
- auxiliary typeclasses that are more unorthodox but still deemed possibly helpful.
Historically, the most prominent member of the project was the monad instance for Try
, since, As We All Know™, Try
does not satisfy all monad laws w.r.t. fatal errors. That has now been included into cats
proper.
However I nevertheless recommend taking a look at the module and see if you can find anything useful.
2) Be disciplined with imports
As you’ve probably learned, either from the documentation, from a certain book, or otherwise, cats
uses a specific hierarchy of imports:
cats.x
for core/"kernel" types;cats.data
for data types such asValidated
, monad transformers, etc.;cats.syntax.x._
for extension method support, so you can call e.g.sth.asRight
,sth.pure
, and so on;cats.instances.x._
for actual implicit scope import of implementation of the various typeclasses for specific types, so that when you call e.g.sth.pure
you don’t get an "implicit not found" error.
And, of course, you’ve noticed the cats.implicits._
import, which merely imports all syntax and all typeclass instances into the implicit scope.
Indeed, normally when developing with cats you should start with the exact sequence of imports from cats’ FAQ, namely:
Once you become more familiar with the library, you may try to mix and match. As a reminder, the rule of thumb is:
cats.syntax.x
gives you an extension syntax that’s relevant tox
,cats.instances.x
gives you the typeclass instances.
For example, if you just want .asRight
, which is an extension method related to Either
, you do:
On the other hand, to have Option.pure
you need to import cats.syntax.monad.
and cats.instances.option.
:
The reason for hand-optimizing your imports is that you limit the implicit scopes in your Scala files, reducing compilation time.
However, please do not do this unless both applies:
- you’re fairly well-versed with cats, and
- your entire team also has that level of familiarity of the lib.
Why? Because:
This happens because cats.implicits
and cats.instances.option
both extend cats.instances.OptionInstances
. We’re essentially importing the implicit scope for that twice, and this confuses the compiler.
The flipside of that is that there is no magic in this implicit hierarchy — it’s a well-determined sequence of type extensions. All you need to do to learn about is to go to the definition of cats.implicits
and then browse through its type hierarchy.
It will take you no more than 10–20 minutes to gain a sufficient level of comprehension in order to avoid problems like these — making it a sound time investment.
tl;dr use
import cats.implicits._
until you’re confident enough not to.
1) Keep your cats up to date on updates!
You may think of your FP library as mostly set in stone, but the fact is that both cats
and scalaz
undergo active development. Again, using cats as the example, these fixes and/or improvements have been relatively recently:
- you don’t have to ascribe an exception to
Throwable
when usingraiseError
anymore, - there are now instances for
Duration
andFiniteDuration
, so you can used1 > d2
without any external libraries. - as well as many other changes and tweaks.
So, check what version of the library you have in your projects, keep yourself in the know about the release notes, and update accordingly.
Closing words
I hope some of the information provided here is novel and helpful to you. Do feel free to add your own ideas about useful, less-known stuff in the comments.
Thanks to Krzysztof Ciesielski for feedback and ideas on including stuff on the list.
Like this post and interested in learning more?
Follow us on Medium!
Need help with your Cassandra, Kafka or Scala projects?
Just contact us here.