Using map vs flatmap for object

807 Views Asked by At

Given that I have a class

class Vehicle(name: String, model: String, age: Int, color: String)

and a Sequence/List

Seq[Vehicle]

Could someone help me with concrete/real-world use case where I would need a map, and a flatmap?

This may be an elementary question - but am yet to find a good example with objects/class use case.

2

There are 2 best solutions below

0
Gastón Schabas On

map and flatMap are not exclusive to collections. You can find those methods in classes like Option, Try, Future.


Let's think in "some real examples" for collections:

  • A company that rent cars only to other companies
  • A monthly report that list all the companies and how many vechiles were rented in a specific month with the columns company and vehicles rented
  • A monthly report for listing all the rented vehicles and which company rented each one with the columns company, vehicle registration plate and model

Our models could look like

case class Vechicle(plate: String, model: String)
case class Company(name: String, vehiclesRented: Seq[Vechicles])

case class ReportTotalVehiclesRentedByCompany(company: String, totalVechiclesRented: Long)
case class ReportRentedVehiclesByCompany(company: String, plate: String, model: String)

A repository that returns a collection of companies who rented vechiles during some month

trait RentedVehiclesRepository {
  def getTotalRentedVehicles(month: Month): Seq[Company]
  def getRentedVehicles(month: Month): Seq[Company]
}

Then a service which returns the report we need

class RentedVehiclesService(repository: RentedVehiclesRepository) {
  def getTotalRentedVehiclesByCompanyReport(month: Month): Seq[ReportTotalVehiclesRentedByCompany] =
    repository
      .getTotalRentedVehicles(month)
      .map(company => 
        ReportTotalVehiclesRentedByCompany(company.name, vehiclesRented.size)
      )

  def getRentedVehiclesByCompanyReport(month: Month): Seq[ReportRentedVehiclesByCompany] =
    repository.getRentedVehicles(month)
      .flatMap(company => // <- here we have the `flatMap`
        company
          .vehiclesRented
          .map(vehicle => // <- here we have the `map`
            ReportRentedVehiclesByCompany(company.name, vehicle.plate, vehicle.model)
          )
      )
}

First we have a method with the signature

def getTotalRentedVehiclesByCompanyReport(month: Month): Seq[ReportTotalVehiclesRentedByCompany]

from the repository we have this one

def getTotalRentedVehicles(month: Month): Seq[Company]

We need to transform what we received from the repository because it's not enough to produce the report. What we want here is to transform a Seq[Company] in Seq[ReportTotalVehiclesRentedByCompany]. Being more abstract would be from a List[A] produce a List[B]. An simple example could be transform a List[Int] into List[String] just using toString method. Comparing OOP vs FP:

  • OOP (using mutable objects)

    1. create new collection
    2. iterate the original one using a for loop
    3. transform each element inside the loop
    4. add the transformed element to the new collection created
    5. return the new collection
      def transform(input: List[Int]): ListBuffer[String] = {
        val newCollection = ListBuffer.empty[String]
        for(i <- input) newCollection += i.toString
        newCollection
      }
      
  • functional programming (using immutable values):

    1. create a function with two parameters (one for the original collection, the second one to accumulate the result)
      def transform(input: List[A], accumulator: List[B]): List[B]
      
    2. call the function passing the original collection and the default result (in this case an empty collection)
      val inputList = List(0,1,2,3,4,5,6,7,8,9)
      val listTransformed: List[String] = transform(inputList, List.empty[String])
      
    3. use pattern matching to split the collection in first element and the rest of the collection
      • if the first parameter is a non empty collection
        1. call the function recursively
        2. pass all the elements of the original collection except the first one as the first parameter
        3. create a new collection containing all the elements in the accumulator plus the new transformed element and use it as the second parameter
      • if the first parameter is an empty collection (means that we don't have nothing else to iterate)
        1. return the result accumulated in the second parameter of the function
      def transform(input: List[Int], accumulator: List[String]): List[String] =
        input match {
          case Nil => 
            accumulator
          case head :: tail => 
            val elementTransformed = head.toString
            val newCollectionWithTheNewElement = accumulator :+ elementTransformed
            transform(tail, newCollectionWithTheNewElement)
        }
      

As I mentioned before, collections have the method map that builds a new sequence by applying a function to all elements of this sequence. Which is what I did in the functional programming example.

Backing to our example of transforming a Seq[Company] to a Seq[ReportTotalVehiclesRentedByCompany], we can just use map to do it. We only need the name of the company and the size of the list of rented vehicles.

In the second report, we have to do something similar, but the problem is we have a collection inside our input collection. If we use map instead of flatMap our result will be a collection of collections (List[List[A]]).

    repository
      .getRentedVehicles(month) // returns a Seq[Company]
      .map(company => // <- using a `map` instead of `flatMap` 
        company
          .vehiclesRented // a Seq[Vechicles]
          .map(vehicle => // returns a Seq[ReportRentedVehiclesByCompany]
            ReportRentedVehiclesByCompany(company.name, vehicle.plate, vehicle.model)
          )
      ) // the result is a Seq[Seq[ReportRentedVehiclesByCompany]]

A simpler example in this case could be a List[List[Int]] transformed to List[String]

def transformNestedList(input: List[List[Int]]): List[String] =
  input
    .flatMap( nestedList => 
      nestedList.map( number => number.toString)
    )

We can observe:

  • map transforms a List[A] in a new List[B]
  • flatMap transforms a List[List[A]] in a new List[B]

The flatMap method applies a map to the element of the collection and then flatten it.

def transformNestedList(input: List[List[Int]]): List[String] =
  input
    .map( nestedList =>
      nestedList.map( number =>
        number.toString
      )                 // <- returns a List[String]
    )                   // <- returns a List[List[String]]
    .flatten            // <- returns a List[String]

As it was mentioned at the beginning, map and flatMap are not exclusive to collections. They come from the functional programming where we have functions with input and output instead of objects with methods.

trait Functor[F[_]] {
  def map[A, B](a: F[A])(f: A => B): F[B]
}
  • flatMap is related with Monad
trait Monad[F[_]] {
  def apply[A](a: A): F[A]
  def flatMap[A,B](a: F[A])(f: A => F[B]): F[B]
}

The F[A] could be think as some kind of container and its API is just map, flatMap and apply. The apply method is just for creating F[A], map and flatMap are the only way to operate over the F[A]. If we think in concret examples of F[A] we could mention Seq[A], Option[A], Try[A] and Future[A]. Each of them represents a concept.

  • Option: represents the absence of a value. It could be None if there is no value, it could be Some(value) when the value is present.
  • Seq: represents a sequence of elements. It could have no elements at all or it could have at least one.
  • Try: represents a computation that can fail. The operation could be Success(value) when succeed or it could be Failure(exception) when it failed

Instead of directly interact with their internal values and asking if they represent one state or the other, you can use map or flatMap to operate them and you don't care about the state.

An "real example" using an Option could be when you have to gather values from different repositories, each repository can bring you the value if the previous one returned a value, if not just return a default value.

class Service(repo1: Repo1, repo2: Repo2, repo3: Repo3, repo4: Repo4) {
  def find(input: Input): Value = {
    val couldBeAValue1: Option[Value1] = repo1.find(input)
    val couldBeAValue2: Option[Value2] = couldBeAValue1.flatMap(value1 => repo2.find(value1))
    val couldBeAValue3: Option[Value3] = couldBeAValue2.flatMap(value2 => repo3.find(value2))
    val couldBeAValue4: Option[Value4] = couldBeAValue3.flatMap(value3 => repo4.find(value3))
    val valueToBeReturned: Value = couldBeAValue4.getOrElse(defaultValue)
  }

  // the previous code could rewrriten using for-comprehension which is 
  // just syntax sugar. The compiler will transform the for-comprehension 
  // in the above code 
  def findForComprehension(input: Input) =
    val couldBeAValue4 = for {
      value1 <- repo1.find(input)
      value2 <- repo2.find(value1)
      value3 <- repo3.find(value2)
      value4 <- repo4.find(value3)
    } yield value4
    val valueToBeReturned: Value = couldBeAValue4.getOrElse(defaultValue)
}

As you can see, instead of asking to each Option value if it is Some or None we just don't care. We only use flatMap and pass the transformation we want to apply as a parameter of the function. If one of them return a None, we will get a None and the following operations will not be executed. The same happens with List. It doesn't matter if it is empty or not. You just apply all the opertions you need using map or flatMap and at the end you extract the final value you got.


These two courses from coursera can help to understand more about these functional concepts

0
Sy.Yah On

Both map and flatMap are higher-order functions in Scala that can be applied to collections like lists, sequences, or arrays. They are used to transform the elements of a collection based on a given function.

map takes a function and applies it to each element of a sequence, returning a new sequence with the results of the function. For example, if you have a sequence of vehicles, you could use map to convert each vehicle object to a string with its name only.

class Vehicle(val name: String, val model: String, val age: Int, val color: String)

val vehicles = Seq(
  new Vehicle("Honda", "Civic", 2023, "Red"),
  new Vehicle("Toyota", "Corolla", 2022, "Blue"),
  new Vehicle("Suzuki", "Swift", 2021, "Black")
)

val vehicleNames = vehicles.map(vehicle => vehicle.name)
println(vehicleNames)

The output is a sequence of strings, with one string for each vehicle in the original sequence:
List(Honda, Toyota, Suzuki)

flatMap is similar to map, but it allows for transforming each element into a collection of elements. It expects the provided function to return a collection (sequence, list, or array) rather than a single element. flatMap then flattens the resulting collection into a single collection by concatenating all the sub-collections. Use flatMap when you want to transform each element into multiple elements or when you want to flatten nested collections. For example, if you have a sequence of vehicles, you could use flatMap to convert each vehicle's name to its character parts.

val vehicleParts = vehicles.flatMap(vehicle => vehicle.name)
println(vehicleParts)

The output of this code is a sequence of all the character parts of names of all the vehicles in the original sequence. Quite meaningless but to demonstrate the flatMap function:
List(H, o, n, d, a, T, o, y, o, t, a, S, u, z, u, k, i)

To illustrate both map and flatMap together, try below code:

val vehicleList = vehicleNames.flatMap { name =>
  vehicles.filter(_.name == name).map(vehicle => s"${vehicle.name} - ${vehicle.model}")
}
println(vehicleList)

flatMap operation is used to iterate over each name in the vehicleNames sequence. For each name, vehicles.find(_.name == name) is used to find the first vehicle with a matching name (find function returns an Option[Vehicle]). Then map is used to transform each matching vehicle into a string representation "${vehicle.name} - ${vehicle.model}". The output is a list of formatted strings representing the vehicle name and model:
List(Honda - Civic, Toyota - Corolla, Suzuki - Swift)