ScalaFP: the Reasons Behind Monads
ScalaFP: the Reasons Behind Monads
Having trouble understanding the monad role in Scala? Click here to learn more about monads and how they work!
Join the DZone community and get the full member experience.
Join For FreeVerify, standardize, and correct the Big 4 + more– name, email, phone and global addresses – try our Data Quality APIs now at Melissa Developer Portal!
Monads play an important role in functional programming. However, they mostly confuse beginners. One of the reasons for the confusion is that they mostly have a knowledge of imperative style programming, where they have no idea about function composition and its importance. In the terms of functional programming, the composition is the root of where we have a set(Z). We compose the elements of the set with the help of functions. For the basic idea of function composition, you can have a walk through my last blog on functors.
Our focus for this post will be on monads. The first question that comes to mind is “Why Monads?”
The answer is that when we have side effects in our program, the monads come into the picture.
What Type of Side Effects Can We Have?
- We are using the database for performing some operations, like CRUD. Every operation can return a different result.
- We are updating the UI, as per our requirements, like adding a button to the frame on the basis of some condition. But, the problem is that the method might return a unit.
- This can include accessing files, read data from the network, and more
The one truth of application development is: without side-effects, there is no possibility of building any applications.
Let’s take an example for performing some pure and side-effect operations. Our requirements are that we need to perform operations like insert, update, find, and send the data to the network one after another. So, in the imperative style, we need to perform the following steps:
addRecordInDatabase(record: Record): Unit = { .... }
updateUI(): Unit = { .... }
val record = findTheLastRecord(): Record = { .... }
sendRecordToTheNetwork(record: Record): Unit = { .... }
In the above example, every method returns a different value. It is also possible that every method might behave differently in the case of failures. In that case, we need to handle the exceptions according to the method. This makes our code bulky and dirty.
How Can we Handle These Situations With Functional Programming?
As we know, functional programming is meant to design or write pure functions without any side-effects. But, the bitter truth of functional programming is that there is no way of designing a side-effected function so that it can be pure. We just create an illusion of pure functional using a wrapper or monads. The output of those functions are the monad of a type. We will explore this in later examples.
Similar to the example discussed earlier where we interacted with the database and the network layer, we need to wrap the output of all the methods in the SomeIO
monad of a type like SomeIO[T]
. This will help us compose the methods easily. So, let’s convert the earlier code into our SomeIO[T]
monad.
addRecordInDatabase(record: Record): SomeIO[Unit] = { .... }
updateUI(): Some[Unit] = { .... }
val record = findTheLastRecord(): SomeIO[Record] = { .... }
sendRecordToTheNetwork(record: Record):SomeIO[Unit] = { .... }
How Monad Helps Us With the Above Workflow? What Are the Benefits of Changing the Return Types?”
First, let’s take a quick look at the compositions again. There are two types of compositions:
- First, the output of one function is the input of another. More information on this can be found in my last blog.
- Second, the intermediate result of one function passes to the next inner function, initially completing the inner function first and then moving to the outer one that we will use in our monads.
Now, we have an idea — monad has a structure that contains some methods. But, before moving to the structure, we need to remember one thing: "monad is a functor." Let’s take the example and structure of the monad shown below:
trait Monad[F[_]] extends Functor {
def pure[A](a: A): F[A]
def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
}
In the above structure, we have a trait named Monad
. But, the F can be a monad that contains any type and two methods, pure
and flatMap
. We will elaborate more on this later, but it also extends a Functor
trait, meaning that the Monad
also contains the map
method. In the meantime, the only thing we need to remember for the monads is that “if the class contains a method map, flatMap
and pure
, it is called a Monad
."
In our Scala core library, we have existing monads like Option[T]
, List[T]
, Future[T]
, and more. All these monads have the pure
method with the different name. For example, using the apply
method, we convert the normal int value to Option[Int]
or List[Int]
, etc. With Future.successfull(1)
, we can create Future[Int]
. So, this means that every library that contains a map
, flatMap
, and pure
(this method name depends on class) can be called a Monad
.
Now, we have an idea about the map
method from the last blog and the pure
method that we saw earlier in this blog. Now, let’s have a look at flatMap
.
flatMap
- As we discussed previously, monads are helping us pass the intermediate result to our next function, if required and perform whatever operation we want to perform. This is only possible because the monads contain the
flatMap
method. -
flatMap
is used when we have a container within another container likeOption[Option[T]]
orList[List[T]]
. So, it pulls out the elements from the inner container and merges it into the outer container. It, then, returns a container of the specific type likeOption[T]
orList[T]
.
Now, we have a brief idea about the monad structure and its methods like pure
, map
, and flatMap
. Let take a look at our earlier examples where monads can help us with method composition.
As we know, we have a SomeIO
monad. The SomeIO
method means that the structure of the monads is something like this:
case class SomeIO[T](value: A) {
def pure(a: A): SomeIO = { ... }
def map[A, B](f: A => B): SomeIO[B] = { ... }
def flatMap[A, B](f: A => SomeIO[B]): SomeIO[B] = { ... }
}
Now, we have our custom monad with all methods required in the monads creation. If you want to explore more about method implementations, look at this video. We have an idea about the map
and flatMap
method, and, if we look into the flatMap
method, it accepts the HOF function that takes A and returns SomeIO[B].
Because of that. flatMap
will return SomeIO[B]
, as well. This means we can write our example like this:
addRecordInDatabase(record).flatMap { _ =>
// perform operation after insert record into database
updateUI().flatMap { _ =>
// perform operation after update ui
findTheLastRecord().flatMap { record =>
// perform operation after fetching the last record
sendRecordToTheNetwork(record).map { _ =>
// perform operation after passes the data to network
}
}
}
}
Here, we are performing an operation one after another as per our requirements with the help of flatMap
and map
method. With the help of the flatMap
method, we are achieving this operation because every method returns a monad of some type. flatMap
is the only method where we can pass HOF with A input and return the monad and function itself with the same monad value.
We can also write the above solution with the monadic operator ( <- ) or Scala for comprehension, which makes our code more concise and readable as below:
for {
_ <- addRecordInDatabase(record)
_ <- updateUI()
record <- findTheLastRecord()
_ <- sendRecordToTheNetwork(record)
} yield ()
Conclusion
Now, we can compose multiple functions easily with the help of monads. These functions may contain side-effects, but we can compose them easily. Scala contains a lot of inbuilt monads. However, sometimes, as per our requirements, we might require to create our custom monad. Or, we may rely on the Scala Cats and Scalaz libraries to provide us with useful monads for writing compositional code. We will be discussing these functions, along with examples, in our future blogs.
Developers! Quickly and easily gain access to the tools and information you need! Explore, test and combine our data quality APIs at Melissa Developer Portal – home to tools that save time and boost revenue.
Published at DZone with permission of Harmeet Singh(Taara) . See the original article here.
Opinions expressed by DZone contributors are their own.
{{ parent.title || parent.header.title}}
{{ parent.tldr }}
{{ parent.linkDescription }}
{{ parent.urlSource.name }}