How to gently introduce an FP library
Tags: scala, functional, tutorial
Published: 2024-02-01 04:23:50

> 📹 Hate reading articles? Check out [the complementary video](https://youtu.be/onFoOrCjxko), which covers the same content.

You cannot bring a library with a whole new paradigm and convince people to use it out of the blue. But you can bring a new library that delivers value right away (and wait for it to spread like wildfire).

We’ll look at a few functionalities we’ve used or seen others use to introduce a first functional dependency into a company.

We’ll use Scala and `cats` in the examples, but it should be somewhat applicable to other libraries and languages.

> Shout out to my colleagues who have done these or supported me on these mischiefs.
> 

## How to combine two maps

Has it ever happened to you that someone gives you a map and then another one, and you have to smoosh them together? For example, maps of scores across different rounds:

```scala
val round1 = Map("A-team" -> 2, "B-team" -> 1)
val round2 = Map("B-team" -> 3, "C-team" -> 2, "D-team" -> 1)
```

If we try something straightforward, like concatenating these maps, the values for the same keys come from the second map (for example, `B-team`):

```scala
round1 ++ round2
// Map(A-team -> 2, B-team -> 3, C-team -> 2, D-team -> 1)
```

If we don’t want to override the values but **combine** (in this case, add) them instead, we can do this by hand — something like this:

```scala
round1.foldLeft(round2){
 case (acc, (k, v)) => acc + (k -> (v + acc.getOrElse(k, 0)))
}

// Map(B-team -> 4, C-team -> 2, D-team -> 1, A-team -> 2)
```

Or using `cats`:

```scala
import cats.implicits._

round1 |+| round2
// Map(B-team -> 4, C-team -> 2, D-team -> 1, A-team -> 2)
```

Or:

```scala
import cats.implicits._

round1.combine(round2)
// Map(B-team -> 4, C-team -> 2, D-team -> 1, A-team -> 2)
```

The `combine` function merges two maps and adds values for the same keys.

On top of that, we can use it with multiple maps:

```scala
val round1 = Map("A-team" -> 2, "B-team" -> 1)
val round2 = Map("B-team" -> 3, "C-team" -> 2, "D-team" -> 1)
val round3 = Map("A-team" -> 1, "C-team" -> 2, "D-team" -> 3)

round1 |+| round2 |+| round3
// Map(A-team -> 3, C-team -> 4, B-team -> 4, D-team -> 4)
```

Or use `combineAll`, which does the same thing:

```scala
List(round1, round2, round3).combineAll
// Map(A-team -> 3, C-team -> 4, B-team -> 4, D-team -> 4)
```

The best part: `combine` knows how to combine a lot of things. For example, we can `combine` maps with lists for values:

```scala
val round1List = Map("A-team" -> List(1, 2), "B-team" -> List(3, 4))
val round2List = 
  Map("A-team" -> List(20), "B-team" -> List(30), "C-team" -> List(0))

round1List.combine(round2List)
// Map(A-team -> List(1, 2, 20), B-team -> List(3, 4, 30), C-team -> List(0))
```

Or even maps with maps with lists for values:

```scala
val day1 = Map("round1" -> round1List)
val day2 = Map("round1" -> round1List, "round2" -> round2List)

day1.combine(day2)
// Map(
//  round1 -> Map(A-team -> List(1, 2, 1, 2), B-team -> List(3, 4, 3, 4)),
//  round2 -> Map(A-team -> List(20), B-team -> List(30), C-team -> List(0))
// )
```

*Sorry, got carried away.*

It’s simple but quite convenient. And, of course, it doesn’t magically work for every type of data and every use-case. You must use supported data types and collections, associative operations, and know what other biases exist.

In other words, why does it sum the integers and doesn’t multiply them, for instance?

```scala
import cats.implicits._

round1.combine(round2)
// Map(B-team -> 4, C-team -> 2, D-team -> 1, A-team -> 2)
```

It takes time to get used to it and figure out. And when we do, we get a powerful tool that we can use in wide variety of use-cases, *even to multiple integers!*

## How to accumulate errors

We’ve all seen these forms that validate one thing at a time. You fill it in, and then it says that there is an error; you fix it, and then it says there is another one, and so on.

Luckily, we can use `Validated` from cats to avoid frustrating our clients. 

If we started with a form-validation with eithers that short-circuit (aka fail fast):

```scala
type Error = String

case class Score(team: String, score: Int)

def validateNameE(name: String): Either[Error, String] =
  if (name.isEmpty) Left("Name cannot be empty")
  else Right(name)

def validateScoreE(score: Int): Either[Error, Int] =
  if (score <= 0) Left("Score must be greater than zero")
  else Right(score)
```

Having both invalid name and score returns only one error (the first one):

```scala
for
  name  <- validateNameE("")
  score <- validateScoreE(0)
yield Score(name, score)
// Left(Name cannot be empty)
```

If we replace this with `Validated`:

```scala
import cats.implicits._
import cats.data.Validated

type Errors = List[String]

def validateName(name: String): Validated[Errors, String] =
  if (name.isEmpty) List("Name cannot be empty").invalid
  else name.valid

def validateScore(score: Int): Validated[Errors, Int] =
  if (score <= 0) List("Score must be greater than zero").invalid
  else score.valid
```

> Note that we use `Errors` instead of `Error`.
> 

We get both errors:

```scala
(validateName(""), validateScore(0)).mapN(Score.apply)
// Invalid(List(Name cannot be empty, Score must be greater than zero))
```

Because we don’t want to short-circuit on the first error, we don’t (and can’t) use for-comprehension. We use `mapN` from cats, which we’ll come back to later. We can only construct a `Score` if both the name and score number are valid.

---

For the record, in real life, `List` isn’t the most efficient collection for concatenations like that — so, we use something more proper, like `NonEmptyChain` from cats:

```scala
import cats.data.NonEmptyChain

type Errors = NonEmptyChain[String]

def validateName(name: String): Validated[Errors, String] =
  if (name.isEmpty) "Name cannot be empty".invalidNec
  else name.validNec

def validateScore(score: Int): Validated[Errors, Int] =
  if (score <= 0) "Score must be greater than zero".invalidNec
  else score.validNec

(validateName(badName), validateScore(badScore)).mapN(Score.apply)
// Invalid(Chain(Name cannot be empty, Score must be greater than zero))
```

> Note that we use `Nec`-suffixed functions, which stand for `NonEmptyChain`.
> 

*Strings aren’t the best for errors either, but anyways…*

---

The `Validated` data type is a great tool for handling validation, which we can use when we need to accumulate errors instead of failing fast and reporting one error at a time.

Nobody wants frustrated clients, right? — easy win.

### Bonus: tidy extension methods

In previous snippets, we used a few extension methods from cats to convert types into `Validated`:  `valid`, `invalid`, `validNec`, `invalidNec`. Those are quite simple but convenient:

```scala
val x = "Same thing".valid

val y = Validated.Valid("Same thing")
```

On top of that, cats provide extension methods for **options** and **eithers** as well. A little syntax change doesn’t justify a whole new dependency. Sure. However, there is one more thing to it:

```scala
Some("this")
// Some[String]

import cats.implicits._

"that".some
// Option[String]
```

These functions return **widened** type (unlike the standard constructors)  

> 💡 Tbh not sure if it still affects type inference too much in Scala 3, but back in the day, it was pretty annoying.
> 

### Bonus: smooshing tuples

Another function we’ve seen is `mapN`. It’s roughly about smooshing tuple values. We can take a tuple of `Validated` values and construct a `Validated` `Score`:

```scala
("A-team".valid[Errors], 2.valid[Errors]).mapN(Score.apply)
// Valid(Score(A-team,2))
```

We can do the same with options:

```scala
case class RoundScore(name: String, round: String, score: Int)

("A-team".some, "round1".some, 2.some).mapN(RoundScore.apply)
// Some(Score(A-team, round1, 2))
```

We can do other stuff, not just constructing; for example, concatenating strings:

```scala
("a".some, "b".some, "c".some).mapN(_ ++ _ ++ _)
// Some(abc)
```

```scala
("a".some, none[String], "c".some).mapN(_ ++ _ ++ _)
// None
```

Or arithmetical operations:

```scala
(1.asRight, 2.asRight, 10.asRight).mapN(_ * _ * _)
// Right(20)
```

And so on. Probably, the most useful is still instantiating case classes.

## How to traverse things of things

The last thing we’ll cover is the `traverse` function.

You might have seen or used the `traverse` or `sequence` to go from many futures to one:

```scala
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

val futureOperations: List[Future[Int]] = List(
  Future { println("Fetch A-team"); 1 },
  Future { println("Fetch B-team"); 2 },
  Future { println("Fetch C-team"); 3 }
)

val result: Future[List[Int]] = Future.sequence(futureOperations)
```

With cats, we can `traverse` and `sequence` many other things. For example, if we have a list of scores that we need to validate:

```scala
import cats.implicits._

def validateScoreE(score: Int): Either[Error, Int] =
  if (score <= 0) Left("Score must be greater than zero")
  else Right(score)

List(-1, 2, -3).traverse(validateScoreE)
// Left(Score must be greater than zero)

List(1, 2, 3).traverse(validateScoreE)
// Right(List(1, 2, 3))
```

*If there is one invalid score, we fail; if all the scores are valid, we successfully get a list of valid scores.*

Or, if we already have a list of eithers, we can convert them into an either with a list for success:

```scala
import cats.implicits._

val input: List[Either[String, Int]] = List(1.asRight, 2.asRight, 3.asRight)

input.sequence
// Right(List(1, 2, 3))
```

*Imagine if you have a list of responses from different services and you want to fail if one of them fails — as soon as one fails, you know that you don’t have to wait for the rest.*

```scala
import cats.implicits._

val input: List[Either[String, Int]] =
  List(1.asRight, "Score must be greater than zero".asLeft, 2.asRight)

input.sequence
// Left(Score must be greater than zero)
```

And once again, it applies to many things, not just eithers and lists — when it clicks, it becomes a powerful tool.

### Bonus: split a list of eithers

While we’re on the topic, another convenient function is `separate`:

```scala
import cats.implicits._

val input: List[Either[String, Int]] =
  List(1.asRight, "Score must be greater than zero".asLeft, 2.asRight)

input.separate
// (List(Score must be greater than zero),List(1, 2))
```

Which separates the values into the *lefts* and *rights*.

## Last note

And there are many more convenient functions and concepts — a lot of ways to tease and introduce a functional library.

A reminder, just in case, try not to bring **unfamiliar to you** libraries to your workplace — it’s not sustainable.

---

{% embed https://youtu.be/onFoOrCjxko %}