Map over result of right-hand side in for comprehension before assigning to value

84 Views Asked by At

I love Scala's for comprehensions! I would love them even more if I could find a way to map over the right-hand side result before assigning it to a value. Say I have the following code:

case class Person(name: String)
def getPersonById(id: Long): Future[Option[Person]] = ???

case class Dog(color: String)
def getDogById(id: Long): Future[Dog] = ???

def addStringToFile(name: Seq[String]): Future[Unit] = ???
val id: Long = ???

for {
  person <- getPersonById(id)
  name = person.map(_.name)

  dog <- getDogById(id)
  color = dog.color

  _ <- addNamesToFile(Seq(name, color).flatten)
} yield name

I don't actually need person or dog as an intermediate value; the are only valuable to the extent that I can extract a property from them on the very next line. Ideally what I'd like to do would look something vaguely like this:

for {
  name <- getPersonById(id))>>.map(_.name)
  color <- getDogById(id))>>.color
  _ <- addNamesToFile(Seq(name, color).flatten)
} yield name

But obviously this isn't valid Scala. Outside of a for-comprenshion I would do:

val name = getPersonById(id).map(_.map(_.name))
val color = getDogById(id).map(_.color)

But the neat thing about the for-comprehension is that I don't need to explicitly perform the outer .map, and the "Future" part is hidden away.

Is it possible to map over the result of a right-hand side expression in a for-comprehension before assigning it to a value?

2

There are 2 best solutions below

0
Dima On BEST ANSWER

Ideally what I'd like to do would look something vaguely like this:

for {
  name <- getPersonById(id))>>.map(_.name)
  color <- getDogById(id))>>.color
  _ <- addNamesToFile(Seq(name, color).flatten)
} yield 

Note that you can actually write exactly that by simply replacing your cryptic )>>. with map :)

  for {
     name <- getPersonBy(id).map(_.map(_.name))
     color <- getDogById(id).map(_.color)
     _ <- addNamesToFile(name.toSeq :+ color)
  } yield name

The only difference between this and what you wrote is .map vs. >> If you like your syntax more for some reason, make an implicit:

object FunnySyntax { 
    implicit class FS[T](val f: Future[T]) extends AnyVal {
       def >>[P] (m: T => P): Future[P] = f.map(m)
     }
}

Now, you can do

name <- getPersonById(id) >> { _.map(_.name) }
color <- getDogById(id) >> { _.color }

This is almost exactly what you had - just a couple of braces and an underscore added ...

1
Dmytro Mitin On

If you don't like person you can replace

for {
  person <- getPersonById(id)
  name   =  person.map(_.name)
  _      <- addNameToFile(name)
} yield name

with

for {
  name <- getPersonById(id).map(_.map(_.name))
  _    <- addNameToFile(name)
} yield name

partially using your another approach

Outside of a for-comprenshion I would do:

getPersonById(id).map(_.map(_.name))

The point of using a for-comprehension is to eliminate just such boilerplate as .map(_.map in the first place!

Sounds like you want monad transformers

import cats.data.OptionT

for {
  person <- OptionT(getPersonById(id))
  name   =  person.name
  _      <- OptionT.liftF(addNameToFile1(name))
} yield name

def addNameToFile1(name: String): Future[Unit] = ???
// def addNameToFile1(name: String): Future[Unit] = addNameToFile(Some(name))

(@JohnyTKoshy advices the same in the comments).

Or without person

for {
  name <- OptionT(getPersonById(id)).map(_.name)
  _    <- OptionT.liftF(addNameToFile1(name))
} yield name

Your new code

for {
  person <- getPersonById(id)
  name   =  person.map(_.name)

  dog    <- getDogById(id)
  color  =  dog.color

  _      <- addStringToFile(Seq(name, Some(color)).flatten)
} yield name

can be re-written without person and dog as

for {
  name  <- getPersonById(id).map(_.map(_.name))
  color <- getDogById(id).map(_.color)
  _     <- addStringToFile(Seq(name, Some(color)).flatten)
} yield name

or with monad transformers as

for {
  name  <- OptionT(getPersonById(id)).map(_.name)
  color <- OptionT.liftF(getDogById(id)).map(_.color)
  _     <- OptionT.liftF(addStringToFile(Seq(name, color)))
} yield name

Ideally what I'd like to do would look something vaguely like this:

for {
  name  <- getPersonById(id))>>.map(_.name)
  color <- getDogById(id))>>.color
  _     <- addNamesToFile(Seq(name, color).flatten)
} yield name

Inventing new syntax instead of .map(_.name), .map(_.color) is an overkill.