9 tips about using cats in Scala you might want to knowMikołaj KoziarkiewiczBlockedUnblockFollowFollowingFeb 6BuzzFeed may be laying people off, but we’re still doing listicles!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 constructorsLet’s start with probably the most basic feature — extension methods for any type that convert an instance into an Option, Either etc.
none for Option.
asLeft for Either,.
invalidNel for ValidatedThe 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.
: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.
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 for productR,and <* for productL.
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.
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) mapNmapN 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.
"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) NestedNested 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(:Apart from Functor, Nested also generalizes operations from Applicative, ApplicativeError and Traverse.
For more information and examples, see here.
valueOrA 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 into A according to the given function,recover is similar, but accepts a partial function, so it can also convert selected errors to A,handleErrorWith is like handleError, but the result is supposed to be F[A], so it effectively also allows you to remap errors,recoverWith is like recover, but also requires F[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.
Task, and others.
Finally, there’s one more method supplied for Either/EitherT, Validated and Ior – .
It’s essentially like .
getOrElse for Option, but generalized for classes that have "something" on the "left" side.
3) alley-catsalley-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.
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 importsAs 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 as Validated, monad transformers, etc.
_ for extension method support, so you can call e.
pure, and so on;cats.
_ for actual implicit scope import of implementation of the various typeclasses for specific types, so that when you call e.
pure you don’t get an "implicit not found" error.
And, of course, you’ve noticed the cats.
_ 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.
x gives you an extension syntax that’s relevant to x,cats.
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.
: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, andyour entire team also has that level of familiarity of the lib.
Why? Because:This happens because cats.
implicits and cats.
option both extend cats.
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.
_ 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 using raiseError anymore,there are now instances for Duration and `FiniteDuration, so you can use d1 > 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 wordsI 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.