© Toby Weston 2018

Toby Weston, Scala for Java Developers, https://doi.org/10.1007/978-1-4842-3108-1_18

18. Monads

Toby Weston

(1)London, UK

Monads are one of those things that people love to talk about but which remain elusive and mysterious. If you’ve done any reading on functional programming, you will have come across the term.

Despite all the literature, the subject is often not well understood, partly because monads come from the abstract mathematics field of category theory and partly because, in programming languages, Haskell dominates the literature. Neither Haskell nor category theory are particularly relevant to the mainstream developer and both bring with them concepts and terminology that can be challenging to get your head around.

The good news is that you don’t have to worry about any of that stuff. You don’t need to understand category theory for functional programming. You don’t need to understand Haskell to program with Scala.

Basic Definition

A layman’s definition of a monad might be:

  • Something that has map and flatMap functions.

This isn’t the full story, but it will serve us as a starting point.

We’ve already seen that collections in Scala are all monads. It’s useful to transform these with map and flatten one-to-many transformations with flatMap. But map and flatMap do different things on different types of monads.

Option

Let’s take the Option class . You can use Option as a way of avoiding nulls, but just how does it avoid nulls and what has it got to do with monads? There are two parts to the answer.

  1. You avoid returning null by returning a subtype of Option to represent no value (None) or a wrapper around a value (Some). As both “no value” and “some value” are of type Option, you can treat them consistently. You should never need to say “if not null”.

  2. How you actually go about treating Option consistently is to use the monadic methods map and flatMap. So, Option is a monad.

We know what map and flatMap do for collections, but what do they do for an Option?

The map Function

The map function still transforms an object, but it’s an optional transformation. It will apply the mapping function to the value of an option, if it has a value. The value and no value options are implemented as subclasses of Option: Some and None respectively (see Figure 18-1).

A456960_1_En_18_Fig1_HTML.jpg
Figure 18-1 The Option classes

A mapping only applies if the option is an instance of Some. If it has no value (that is, it’s an instance of None), it will simply return another None.

This is useful when you want to transform something but not worry about checking if it’s null. For example, we might have a Customers trait with repository methods add and find. What should we do in implementations of find when a customer doesn’t exist?

  trait Customers extends Iterable[Customer] {
    def add(Customer: Customer)
    def find(name: String): Customer
  }

A typical Java implementation would likely return null or throw some kind of NotFoundException. For example, the following Set-based implementation returns a null if the customer cannot be found:

  import scala.collections._

  class CustomerSet extends Customers {
    private val customers = mutable.Set[Customer]()


    def add(customer: Customer) = customers.add(customer)

    def find(name: String): Customer = {
      for (customer <- customers) {
        if (customer.name == name)
          return customer
      }
      null
    }


    def iterator: Iterator[Customer] = customers.iterator
  }

Returning null and throwing exceptions both have similar drawbacks.

Neither communicate intent very well. If you return null, clients need to know that’s a possibility so they can avoid a NullPointerException. But what’s the best way to communicate that to clients? ScalaDoc? Ask them to look at the source? Both are easy for clients to miss. Exceptions may be somewhat clearer but, as Scala exceptions are unchecked, they’re just as easy for clients to miss.

You also force unhappy path handling to your clients. Assuming that consumers do know to check for a null, you’re asking multiple clients to implement defensive strategies for the unhappy path. You’re forcing null checks on people and can’t ensure consistency, or even that people will bother.

Defining the find method to return an Option improves the situation. In the following, if we find a match, we return Some customer or None otherwise. This communicates at an API level that the return type is optional. The type system forces a consistent way of dealing with the unhappy path.

  trait Customers extends Iterable[Customer] {
    def add(Customer: Customer)
    def find(name: String): Option[Customer]
  }

Our implementation of find can then return either a Some or a None.

  def find(name: String): Option[Customer] = {
    for (customer <- customers) {
      if (customer.name == name)
        return Some(customer)
    }
    None
  }

Let’s say that we’d like to find a customer and get their total shopping basket value. Using a method that can return null, clients would have to do something like the following, as Albert may not be in the repository.

  val albert = customers.find("Albert")           // can return null
  val basket = if (albert != null) albert.total else 0D

If we use Option, we can use map to transform from an option of a Customer to an option of their basket value.

  val basketValue: Option[Double] =
    customers.find("A").map(customer => customer.total)

Notice that the return type here is an Option[Double]. If Albert isn’t found, map will return a None to represent no basket value. Remember that the map on Option is an optional transformation.

When you want to actually get hold of the value, you need to get it out of the Option wrapper. The API of Option will only allow you call get, getOrElse or continue processing monadically using map and flatMap.

Option.get

To get the raw value, you can use the get method but it will throw an exception if you call it against no value. Calling it is a bit of a smell as it’s roughly equivalent to ignoring the possibility of a NullPointerException. You should only call it when you know the option is a Some.

  // could throw an exception
  val basketValue = customers.find("A").map(customer => customer.total).get

To ensure the value is a Some, you could pattern match like the following, but again, it’s really just an elaborate null check.

  val basketValue: Double = customers.find("Missing") match {
    case Some(customer) => customer.total          // avoids the exception
    case None => 0D
  }

Option.getOrElse

Calling getOrElse is often a better choice as it forces you to provide a default value. It has the same effect as the pattern match version, but with less code.

  val basketValue =
    customers.find("A").map(customer => customer.total).getOrElse(0D)

Monadically Processing Option

If you want to avoid using get or getOrElse, you can use the monadic methods on Option. To demonstrate this, we need a slightly more elaborate example. Let’s say we want to sum the basket value of a subset of customers. We could create the list of names of customers we’re interested in and find each of these by transforming (mapping) the customer names into a collection of Customer objects.

In the following example, we create a customer database, adding some sample data before mapping.

  val database = new CustomerSet()

  val address1 = Some(Address("1a Bridge St", None))
  val address2 = Some(Address("2 Short Road", Some("AL1 2PY")))
  val address3 = Some(Address("221b Baker St", Some("NW1")))


  database.add(Customer("Albert", address1))
  database.add(Customer("Beatriz", None))
  database.add(Customer("Carol", address2))
  database.add(Customer("Sherlock", address3))


  val customers = Set("Albert", "Beatriz", "Carol", "Dave", "Erin")
  customers.map(database.find(_))

We can then transform the customers again to a collection of their basket totals.

  customers.map(database.find(_).map(_.total))

Now here’s the interesting bit. If this transformation were against a value that could be null, and not an Option, we’d have to do a null check before carrying on. However, as it is an option, if the customer wasn’t found, the map would just not do the transformation and return another “no value” Option.

When, finally, we want to sum all the basket values and get a grand total, we can use the built-in function sum.

  customers.map(database.find(_).map(_.total)).sum       // wrong!

However, this isn’t quite right. Chaining the two map functions gives a return type of Set[Option[Double]], and we can’t sum that. We need to flatten this down to a sequence of doubles before summing.

  customers.map(database.find(_).map(_.total)).flatten.sum
                                       ^
              notice the position here, we map immediately on Option

The flattening will discard any Nones, so afterwards the collection size will be 3. Only Albert, Carol, and Beatriz’s baskets get summed.

The Option.flatMap Function

In the preceding example, we replicated flatMap behavior by mapping and then flattening, but we could have used flatMap on Option directly .

The first step is to call flatMap on the names instead of map. As flatMap does the mapping and then flattens, we immediately get a collection of Customer.

  val r: Set[Customer] = customers.flatMap(name => database.find(name))

The flatten part drops all the Nones, so the result is guaranteed to contain only customers that exist in our repository. We can then simply transform those customers to their basket total, before summing.

  customers
    .flatMap(name => database.find(name))
    .map(customer => customer.total)
    .sum

Dropping the no value options is a key behavior for flatMap here. For example, compare the flatten on a list of lists as follows:

  scala> val x = List(List(1, 2), List(3), List(4, 5))
  x: List[List[Int]] = List(List(1), List(2), List(3))


  scala> x.flatten
  res0: List[Int] = List(1, 2, 3, 4, 5)

…to a list of options.

  scala> val y = List(Some("A"), None, Some("B"))
  y: List[Option[String]] = List(Some(A), None, Some(B))


  scala> y.flatten
  res1: List[String] = List(A, B)

More Formal Definition

As a more formal definition, a monad must:

  • Operate on a parameterized type, which implies it’s a “container” for another type (this is called a type constructor).

  • Have a way to construct the monad from its underlying type (the unit function).

  • Provide a flatMap operation (sometimes called bind).

Option and List both meet these criteria , as shown in Table 18-1.

Table 18-1 Monad criteria met by Option and List
 

Option

List

Parameterized (type constructor)

Option[A]

List[T]

Construction (unit)

Option.apply(x)

List(x, y, z)

 

Some(x)

 
 

None

 

flatMap (bind)

def flatMap[B](f: A => Option[B]): Option[B]

def flatMap[B](f: A => List[B]): List[B]

The definition doesn’t mention map, though, and our layman’s definition for monad was the following:

  • Something that has map and flatMap functions.

I wanted to introduce flatMap in terms of map because it always applies a mapping function before flattening. It’s true that to be a monad you only have to provide flatMap, but in practice monads also supply a map function. This is because all monads are also functors ; it’s functors that more formally have to provide maps.

So, the technical answer is that providing flatMap, a parameterized type, and the unit function makes something a monad. But all monads are functors and map comes from functor (see Figure 18-2).

A456960_1_En_18_Fig2_HTML.jpg
Figure 18-2 The Functor and Monad behaviors

Summary

In this chapter, I explained that when people talk about monadic behavior, they’re really just talking about the map and flatMap functions. The semantics of map and flatMap can differ depending on the type of monad but they share a formal, albeit abstract, definition.

We looked at some concrete examples of the monadic functions on List and Option, and how we can use these with Option to avoid null checks. The real power of monads, though, is in “chaining” these functions to compose behavior into a sequence of simple steps. To really see this, we’re going to look at some more elaborate examples in Chapter 19, and see how for comprehensions work under the covers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.254.118