Download as pdf or txt
Download as pdf or txt
You are on page 1of 8

44 Working with Future: map and

flatMap

After reading this lesson, you will be able to

Manipulate the result of an asynchronous computation using the map


operation
Merge two nested asynchronous computations using the flatten
method
Combine multiple asynchronous operations using the flatMap
function

In the previous lesson, you learned the basics of expressing asynchronous


computations using the type Future . In this lesson, you’ll learn how to
use the methods map , flatten , and flatMap for an instance of
Future . You’ll notice that they share many commonalities with the map ,
flatten , and flatMap methods you have mastered for other types.
The map function allows you to transform the value that an asyn-
chronous computation produces. The flatten method merges two nest-
ed instances of Future into one. The flatMap operation is the composi-
tion of the methods map and flatten , and it allows you to chain multi-
ple asynchronous instances. In the capstone, you’ll need to coordinate
several asynchronous calls to read or write questions to a database for
your quiz application.

Consider this

Imagine that your application to book tickets for events performs an


asynchronous computation to produce a registration receipt. After its
completion, you’d like to show the user a message to confirm its registra-
tion number and provide more details about the event. How would you
achieve this?

44.1 The map, flatten, and flatMap operations

The type Future has an implementation for the map , flatten , and
flatMap methods. You’ll see them in action in the following subsections.
You will notice that they have many similarities with those you are al-
ready encountered, such as Option , List , and Try .

44.1.1 The map function

Let’s consider again your program to place orders in a store. Suppose that
after checking for a product’s availability, you’d like to either place an or-
der or reject the request.

Listing 44.1 Placing an order if the product is available

import scala.concurrent.Future
import scala.concurrent.ExecutionContext
case class Availability(id: Int, quantity: Double)
case class Order(id: Int,
customerId: Int,
productId: Int,
quantity: Double)

private def getAvailability(productId: Int)


(using ec: ExecutionContext): Future[Availability] = ???

private def createOrder(customerId: Int,


productId: Int,
quantity: Double): Order = ???

def placeOrder(customerId: Int,


productId: Int,
quantity: Double)
(using ec: ExecutionContext): Future[Order] = {
getAvailability(productId).map { availability =>
if (quantity <= availability.quantity)
createOrder(customerId = customerId,
productId = productId,
quantity)
else throw new IllegalStateException(
s"Product $productId unavailable: " +
s"requested $quantity, available ${availability.quantity}")
}
}

It accesses the value wrapped into a Future instance, and it applies a


function to it.

Future catches any exception; you can throw them knowing that Future
will contain them.

When working with an asynchronous computation, you can use its


method map to transform its produced result. For an instance of
Future[T] , the function map takes one parameter f of type T => S
and an implicit execution context to produce an instance of type
Future[S] . It has the following signature:

def map[S](f: T => S)(using ec: ExecutionContext): Future[S]

If your Future[T] instance has completed successfully, it will apply the


parameter f to its result to produce a value of type Future[S] . Nothing
happens if your Future[T] instance has completed with a failure. A few
examples of how to use it are the following:

scala> import scala.concurrent.Future


import scala.concurrent.Future

scala> import scala.concurrent.ExecutionContext.Implicits.global


import scala.concurrent.ExecutionContext.Implicits.global

scala> Future(12/2).map(_ * 3)
val res0: scala.concurrent.Future[Int] = Future(Success(18))

scala> Future(12/0).map(_ * 3)
val res1: scala.concurrent.Future[Int] = Future(Failure(java.lang.ArithmeticException: / b

scala> Future(12/2).map { n =>


| if (n > 10) n
| else throw new Exception(s"Too small: $n")
| }
val res2: scala.concurrent.Future[Int] = Future(Failure(
java.lang.Exception: Too small: 6))

QUICK CHECK 44.1 Define a function called toInt to parse a val-


ue of type Future[String] into one of Future[Int] . Provide an exe-
cution context as an implicit parameter rather than importing one di-
rectly.

44.1.2 The flatten function

Consider the function you wrote in listing 44.1 to check the availability
for a product and create an order. Its function createOrder returns a
value of type Order . Imagine the function createOrder now needs to
write to a database asynchronously and that you need to change its re-
turn type from Order to Future[Order] . This causes its function
placeOrder to return a value of type Future[Future[Order]] .

Listing 44.2 Placing an order by writing to a database

private def getAvailability(productId: Int)


(using ec: ExecutionContext): Future[Availability] = ???

private def createOrder(customerId: Int,


productId: Int,
quantity: Double)
(using ec: ExecutionContext): Future[Order] = ???

def placeOrder(customerId: Int,


productId: Int,
quantity: Double)
(using ec: ExecutionContext): Future[Future[Order]] = {
getAvailability(productId).map { availability =>
if (quantity <= availability.quantity)
createOrder(customerId = customerId, productId = productId, quantity)
else throw new IllegalStateException(
s"Product $productId unavailable: " +
s"requested $quantity, available ${availability.quantity}")
}
}

The type Future[Future[Order]] represents two nested asynchronous


computations that will eventually either return an instance of type
Order or fail. The function flatten can simplify this expression by
considering the two nested operations as one; it will now produce a value
of type Future[Order] instead.

Listing 44.3 Placing an order by writing to a database using flatten

private def getAvailability(productId: Int)


(using ec: ExecutionContext): Future[Availability] = ???
private def createOrder(customerId: Int,
productId: Int,
quantity: Double)
(using ec: ExecutionContext): Future[Order] = ???

def placeOrder(customerId: Int,


productId: Int,
quantity: Double)
(using ec: ExecutionContext): Future[Order] = {
getAvailability(productId).map { availability =>
if (quantity <= availability.quantity)
createOrder(customerId = customerId,
productId = productId,
quantity)
else throw new IllegalStateException(
s"Product $productId unavailable: " +
s"requested $quantity, available ${availability.quantity}")
}.flatten
}

The method flatten on Future allows you to transform an instance of


Future[Future[T]] into one of type Future[T] . A few examples are
following:

scala> import scala.concurrent.Future


import scala.concurrent.Future

scala> import scala.concurrent.ExecutionContext.Implicits.global


import scala.concurrent.ExecutionContext.Implicits.global

scala> val twelveOverZero = Future(Future(12/0)).flatten


val twelveOverZero: scala.concurrent.Future[Int] = Future(<not completed>)

scala> twelveOverZero
val twelveOverZero: scala.concurrent.Future[Int] = Future(Failure(java.lang.ArithmeticExce

scala> Future(Future(12/2)).flatten
val res0: scala.concurrent.Future[Int] = Future(Success(6))

scala> Future(5).flatten
error: Cannot prove that Int <:< scala.concurrent.Future[S].
// You can only invoke the method flatten on nested structures

QUICK CHECK 44.2 Consider the following snippet of code:

import scala.concurrent.{ExecutionContext, Future}

case class Account(id: String)


case class User(name: String)

def getAccount(orderId: Int)


(using ec: ExecutionContext): Future[Account] = ???

def getUser(accountId: String)


(using ec: ExecutionContext): Future[User] = ???

Use the functions getAccount and getUser to create a new function


that will return the user associated with a given order ID. This function
should have the following signature:

def getUser(orderId: Int)


(using ec: ExecutionContext): Future[User]
44.1.3 The flatMap function

Consider the snippet of code you wrote in listing 44.3. There is a more ele-
gant way of achieving the same result.

Listing 44.4 Placing an order by writing to a database using flatMap

private def getAvailability(productId: Int)


(using ec: ExecutionContext): Future[Availability] = ???

private def createOrder(customerId: Int,


productId: Int,
quantity: Double)
(using ec: ExecutionContext): Future[Order] = ???

def placeOrder(customerId: Int,


productId: Int,
quantity: Double)
(using ec: ExecutionContext): Future[Order] = {
getAvailability(productId).flatMap { availability =>
if (quantity <= availability.quantity)
createOrder(customerId = customerId,
productId = productId,
quantity)
else throw new IllegalStateException(
s"Product $productId unavailable: " +
s"requested $quantity, available ${availability.quantity}")
}
}

The method flatMap is the combination of the map and flatten oper-
ations. For an instance of Future[T], the function flatMap takes one
parameter f of type T => Future[S] and an implicit execution con-
text to produce an instance of type Future[S] . It has the following
signature:

def flatMap[S](f: T => Future[S])


(using ec: ExecutionContext): Future[S]

If your instance of Future[T] has completed successfully, it will apply


the parameter f to produce a value of type Future[S] . Nothing hap-
pens if your instance has completed with a failure. A few examples of
how to use it are the following:

scala> import scala.concurrent.Future


import scala.concurrent.Future

scala> import scala.concurrent.ExecutionContext.Implicits.global


import scala.concurrent.ExecutionContext.Implicits.global

scala> val twelveOverTwo = Future(12/2).flatMap(n => Future(n.toString))


val twelveOverTwo: scala.concurrent.Future[String] = Future(<not completed>)
// twelveOverTwo has not completed yet – let give it another try

scala> twelveOverTwo
val twelveOverTwo: scala.concurrent.Future[String] = Future(Success(6))
// twelveOverTwo has now completed successfully

scala> Future(12/0).flatMap(n => Future(n.toString))


val res0: scala.concurrent.Future[String] = Future(Failure(java.lang.ArithmeticException:
The flatMap method allows you to express an execution dependency:
asynchronous computations. For example, the placeOrder function you
implemented in listing 44.4 defines that the product availability check
must complete successfully before creating an order. You will learn more
about this in the next lesson where you will master how to use for-com‐
prehension on instances of type Future .

QUICK CHECK 44.3 In Quick Check 44.2, you implemented a func-


tion getUser(orderId: Int) using the function flatten . Refactor it
to use the flatMap method instead.

Table 44.1 summarizes the signature and usage of the methods map ,
flatten , and flatMap acting on Future .

Table 44.1 Technical recap of the three fundamental operations on the


type Future . The function map transforms the result of an asyn-
chronous computation while flatten merges two executions. The
flatMap function combines the map and flatten operations to define
an execution order between values.

Acts on Signature Usage

map Future[T] map(f: T => S)(using It applies


ec:ExecutionContext): a function
Future[S] to the val-
ue the fu-
ture
produced.

flat‐ Future[ flatten: Future[T] It merges


ten Future[T]] two nest-
ed futures
into one.

flat‐ Future[T] flatMap(f: T => S) The com-


Map (using bination
ec:ExecutionContext): of map
Future[S] followed
by flat‐
ten
chains fu-
tures
together.

Summary

In this lesson, my objective was to teach you the fundamental operations


you can perform on an instance of type Future .

You saw how to use the function map to transform the value your
asynchronous computation produces.
You learned how to merge two nested instances of Future into one
using the flatten operation.
You discovered how the flatMap method allows you to express an
execution dependency between two asynchronous computations.
Let’s see if you got this!

TRY THIS Consider the following snippet of code that defines a func-
tion to list the content in a given directory:

import java.io.File
import scala.concurrent.{ExecutionContext, Future}

def contentInDir(path: String)


(using ec: ExecutionContext): Future[List[String]] =
Future {
val file = new File(path)
if (file.isDirectory)
// unfortunately, listFiles returns null
// if invoked on a file that is not directory
file.listFiles().toList.map(_.getAbsolutePath)
else List.empty
}

Define a new function that invokes the function contentInDir to count


the number of items in a directory.

Answers to quick checks

QUICK CHECK 44.1 Your function toInt should have a signature


and implementation similar to the following:

import scala.concurrent.{ExecutionContext, Future}

def toInt(f: Future[String])


(using ec: ExecutionContext): Future[Int] =
f.map(_.toInt)

QUICK CHECK 44.2 A possible implementation for the function


getUser(orderId: Int) is the following:

def getUser(orderId: Int)


(using ec: ExecutionContext): Future[User] =
getAccount(orderId).map(account => getUser(account.id)).flatten

QUICK CHECK 44.3 You should refactor your function


getUser(orderId: Int) as follows:

def getUser(orderId: Int)


(using ec: ExecutionContext): Future[User] =
getAccount(orderId).flatMap(account => getUser(account.id))

You might also like