top of page
  • Writer's pictureFusionpact

Functional Web APIs using Scala Cats Library: Full Guide



Hello everyone and welcome to our new blog series which explores the Cats Library in Scala. This blog series is divided into three parts, the first part is here to focus on the basics of the CATS library, the second part is going to touch upon more intermediate topics of scala and we are going to discuss the foundation of the demo application that we are going to build which is a straightforward Function Web API. So get ready and if you haven’t subscribed to our newsletter now will be a good time to do just that because we will drop new technology wisdom on you weekly.


What is Cats?

So, what is Cats, this library contains type classes that are used to provide abstraction for functional programming concepts. Type classes in Scala can be defined as a way to create additional functionality for the already existing types. Type classes in Scala are defined by a trait having a single type parameter. A type parameter can be thought of in the same way as Generics in Java. If we are writing generic code in Java, for example, let us say that we are defining a list in Java: List<T> list = new ArrayList<>(); here the T parameter is used for everyone such as Strings, Ints, Chars, and all the other data types, this does not give us the ability to provide specific functionality to specific data types, what if we just wanted to provide the functionality to only Strings and the Ints type, enter Type Classes. Cats is a library that is full of these type classes in order to provide abstractions for functional programming concepts in Scala.


We are going to look at some of the most widely used type classes down below:


What is the problem solved by Cats?

The Cats library in Scala solves several problems commonly encountered in functional programming:


1. Abstracting over data types: Cats provides type classes, such as Functor, Applicative, and Monad, that allow you to write generic code that works with different data types. These classes we are going to describe in the next section of the blog. This abstraction enables you to write reusable and polymorphic functions, decoupling the implementation details from the specific data types used.


2. Enabling type-safe ad hoc polymorphism: Cats' type classes allow you to define behaviors for a wide range of data types without modifying their original implementations. This ad hoc polymorphism provides a flexible and type-safe way to extend the functionality of existing types. If you google type classes, it will describe type classes as a powerful tool that is used in functional programming which enables ad-hoc polymorphism, we will discuss this below as well.


3. Simplifying error handling: Cats provides constructs like Either, Validated, and Option that simplify error handling. With these constructs, you can handle and propagate errors in a composable and expressive manner, avoiding exceptions and improving code reliability.


4. Managing side effects: Cats offers effect types like IO and Task, which provide a functional and composable way to manage side effects in a pure and predictable manner. These effect types enable you to separate the description of effects from their execution, making it easier to reason about and test your code.


5. Providing functional data structures: Cats include various functional data structures, such as NonEmptyList, ValidatedNel, and ValidatedNec, which help you work with data in a functional way. These structures offer benefits like immutability, type safety, and better composability, enhancing the reliability and maintainability of your code.


6. Typeclass derivation: Cats supports the automatic derivation of type class instances for custom data types using the `cats.derived` module. This feature eliminates the need to manually write repetitive and boilerplate code, reducing development time and improving code maintainability.



Ad-hoc polymorphism

Ad hoc polymorphism, also known as function overloading, allows a function or method to have different implementations based on the types of its arguments. It enables us to use the same function name to perform different operations depending on the argument types.


In Scala, ad hoc polymorphism is achieved through type classes and the use of the `implicit` keyword. A type class defines a set of operations that can be performed on a certain type. Implicit instances of the type class provide specific implementations of these operations for different types.


The `implicit` keyword allows us to mark objects as implicit instances of a type class. When a function or method requires an implicit parameter of a certain type class, the Scala compiler searches for an appropriate implicit instance in the current scope.


By combining ad hoc polymorphism, type classes, and implicits, we can define generic functions that can operate on various types, while allowing different implementations to be automatically selected based on the available implicit instances.



Applicatives


Applicatives are a type class in functional programming that provides a way to combine independent computations within a context. They generalize the concept of function application to work with values wrapped in a context, allowing us to apply functions to multiple independent values and capture the results within the same context.


In simple terms, an Applicative allows us to perform operations on values within a context, such as Option, Either, or Future while preserving the context itself. It enables us to work with multiple values in parallel or sequence them based on their context.


Here's a small code example in Scala to illustrate the concept of Applicatives using the `Option` context:



import cats.Applicative
import cats.implicits._

val add: (Int, Int) => Int = _ + _

val option1: Option[Int] = Some(2)
val option2: Option[Int] = Some(3)

val result: Option[Int] = Applicative[Option].map2(option1, option2)(add)
println(result) // Output: Some(5)

```


In the example above, we define a function `add` that adds two `Int` values. We also create two `Option` values, `option1` and `option2`, representing optional integers.


Using the `Applicative` type class, we can combine the two `Option` values using the `map2` method. The `map2` method takes two independent computations (`option1` and `option2`) and a function (`add`) as arguments. It applies the function to the values within the context and returns a new `Option` with the result.


The `Applicative[Option]` instance provides the implementation of the `map2` method for the `Option` context. It handles the specifics of how the function should be applied to the wrapped values and how to handle the `None` case if either of the options is empty.


In the example, the `result` variable holds the combined result of applying the `add` function to the values within the `option1` and `option2` contexts. Since both options are non-empty, the result is `Some(5)`.


Applicatives are powerful tools for working with computations within a context. They allow us to perform operations on independent values while preserving the context and capturing the results. This makes them particularly useful in scenarios involving optional values, error handling, parallel computations, and more.


Functors

They are the type classes that are used to wrap over type constructors such as List, Options, Future, etc.



Montrait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

// Example implementation for Option
implicit val functorForOption: Functor[Option] = new Functor[Option] {
  def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa match {
    case None    => None
    case Some(a) => Some(f(a))
  }
}ads


As you must have noticed here that we are using the implicit call above as this is the convention we use in order to create instances of type classes.


Semigroup

Certainly! Semigroups are another type class in functional programming that provides a way to combine or concatenate values. Unlike Applicatives, Semigroups focus solely on the operation of combining values and do not provide a context.


In simple terms, a Semigroup defines an associative binary operation that takes two values of the same type and combines them into a single result. The operation is associative, meaning that the order in which the values are combined does not matter.


Here's a small code example in Scala to illustrate the concept of Semigroups using the `Int` type:



import cats.Semigroup
import cats.implicits._
val add: (Int, Int) => Int = _ + _
val value1: Int = 2
val value2: Int = 3
val result: Int = Semigroup[Int].combine(value1, value2)
println(result) // Output: 5



In the example above, we define a function `add` that adds two `Int` values. We also create two `Int` values, `value1` and `value2`, representing integer values.


Using the `Semigroup` type class, we can combine the two `Int` values using the `combine` method. The `combine` method takes two values of the same type (`value1` and `value2`) and applies the associative operation defined by the `Semigroup` instance. In this case, the operation is addition (`add` function).


The `Semigroup[Int]` instance provides the implementation of the `combine` method for the `Int` type. It handles the specifics of how the addition operation should be performed.


In the example, the `result` variable holds the combined result of applying the addition operation to `value1` and `value2`. Since addition is associative, the order in which the values are combined does not matter, and the result is `5`.


Semigroups are useful for combining values of the same type in an associative manner. They provide a generic way to express concatenation or combination operations and can be used in various contexts, such as merging data structures, aggregating values, or composing functions. From the above, Semigroup and Applicatives might look a lot similar but that is not the case, Applicatives extends Functors with ap and pure method.


They key differences between the two can be stated as Semigroups and Applicatives are two different type classes in functional programming with distinct purposes. Semigroups are concerned with defining an associative binary operation that combines two values of the same type into a single result. They focus solely on the operation of combining values and do not provide a context or handle empty values. Examples of semigroup operations include the addition of numbers or concatenation of strings. On the other hand, Applicatives allow the application of functions within a context, such as Option or Either, to values of the same context while preserving the context itself. They enable parallel or sequential composition of computations within the context and handle context-specific scenarios like handling missing or empty values. Applicatives are useful for scenarios involving optional values, error handling, or combining results of independent computations. In summary, Semigroups deal with combining values in an associative manner, while Applicatives focus on applying functions within a context while maintaining the context and handling specific contextual scenarios.


Monad

Monads are another type class in functional programming that provide a way to sequence and compose computations that involve a context. They allow us to work with values within a context, perform operations on them, and manage the flow of computations.


In simple terms, a Monad allows us to wrap a value in a context and then apply a sequence of operations to that value while preserving the context. The result of each operation is automatically wrapped back into the context, allowing for seamless chaining of operations.



val divide: (Int, Int) => Option[Int] = (a, b) => if (b != 0) Some(a / b) else None

val result: Option[Int] = Some(10).flatMap { x =>
  Some(2).flatMap { y =>
    divide(x, y)
  }
}

println(result) // Output: Some(5)


In this example, we define the divide function that performs integer division, just like in the previous example. We have two values, 10 and 2, wrapped in Some contexts.


Using the flatMap method, we can chain the operations directly. Each flatMap call extracts the value from the Option context and applies the subsequent operation. If any of the values result in None, the entire expression evaluates to None. Otherwise, the final result is wrapped in Some.


The result variable holds the final result of the division operation. Since both values are non-zero, the division is successful and the result is Some(5).


While using a for comprehension often provides a more concise and readable syntax, directly chaining flatMap calls showcases the underlying mechanism of monads in functional programming.


Alternatives

Alternatives, also known as "OrElse" or "Fallback" semantics, are a concept in functional programming that provide a way to handle and combine computations that may result in a value or a default alternative. They are often used in scenarios where we want to provide a fallback or default value when a computation fails or produces an empty result.


In simple terms, Alternatives allow us to specify a primary computation and an alternative computation. If the primary computation succeeds and produces a value, that value is returned. However, if the primary computation fails or produces an empty result, the alternative computation is executed and its result is returned instead.


Here's a small code example in Scala to illustrate the concept of Alternatives using the `Option` context:



val primary: Option[String] = Some("Hello")
val alternative: Option[String] = None
val result: Option[String] = primary.orElse(alternative)
println(result) // Output: Some("Hello")



In the example above, we have a primary `Option[String]` called `primary` that holds the value `Some("Hello")`. We also have an alternative `Option[String]` called `alternative` that is empty (`None`).


Using the `orElse` method, we combine the primary and alternative computations. If the primary computation (`primary`) has a value, it is returned as the result. However, if the primary computation is empty (`None`), the alternative computation (`alternative`) is evaluated, and its result becomes the final output.


In this case, since the primary computation (`primary`) has a value, the result is `Some("Hello")`.


Alternatives provide a way to handle fallback or default values when a computation fails or produces an empty result. They allow for flexible control flow and provide a mechanism to handle different outcomes in a concise and expressive manner.


This is all for the first part of this series where we have looked at some of the basic concepts of cats library talking about the type classes that make up the library in the next edition we will talk about some more advanced mechanisms and we will start to develop a Functional Web API so make sure you are subscribed to our newsletter.


For any queries contact, hello@fusionpact.com








37 views0 comments
bottom of page