John Doe
John Doe

Reputation: 275

How to merge two Option[String] variables into one when using flatMap in Scala?

I have the following class:

case class Profile(email: Option[String],
                   firstName: Option[String],
                   lastName: Option[String],
                   fullName: Option[String])

Now I want to remove the fullName attribute because it's redundant. However, I have a method in my class User which returns the fullName:

case class User(id: UUID, profiles: List[Profile]) {
// Skipped some lines
  def fullName(loginInfo:LoginInfo) = profileFor(loginInfo).flatMap(_.fullName)
}

Now I am trying to replace the .flatMap(_.fullName) part with a concatenation of firstName + lastName. How can this be done? Do I need make a new Option[String], like this:

def fullName(loginInfo:LoginInfo) = {
  val firstName = profileFor(loginInfo).flatMap(_.firstName)
  val lastName = profileFor(loginInfo).flatMap(_.lastName)
  val fullName : Option[String] = Some(firstName + " " + lastName)
  fullName
}

Upvotes: 6

Views: 2967

Answers (6)

Yuval Itzchakov
Yuval Itzchakov

Reputation: 149656

I actually wrote a blog post not long ago with a couple of native Scala options to do that.

The one I liked the most is similar to @Pawels answer only using reduceLeftOption:

scala> val firstName = Some("yuval")
firstName: Some[String] = Some(yuval)

scala> val lastName = Some("itzchakov")
lastName: Some[String] = Some(itzchakov)

scala> (firstName ++ lastName).reduceLeftOption((a,b) => s"$a $b")
res10: Option[String] = Some(yuval itzchakov)

This approach is nice because it works when either of the Option[T] is None:

scala> val lastName: Option[String] = None
lastName: Option[String] = None

scala> (firstName ++ lastName).reduceLeftOption((a,b) => s"$a $b")
res11: Option[String] = Some(yuval)

Another nice property of this is that it can work for N elements when using varargs as well:

scala> :paste
// Entering paste mode (ctrl-D to finish)

def reduce[T](options: Option[T]*)(f: (T, T) => T) = {
  options.flatten.reduceLeftOption(f)
}
reduce(Some(1), Some(1), Some(2), Some(4))(_+_)

// Exiting paste mode, now interpreting.

reduce: [T](options: Option[T]*)(f: (T, T) => T)Option[T]
res0: Option[Int] = Some(8)

Upvotes: 2

Łukasz
Łukasz

Reputation: 8673

I think from oop perspective, you should keep fullName in Profile class, but as a method and from your User class just delegate it to Profile.

You can use any solution already given, I will post mine, using scalaz and applicative builder, it is similar to map2, just more general, as you can join any (well, not really any, but a reasonable amount as you have to pass the function that takes same number of arguments as the number of objects you want apply it to) number of options or other applicatives.

@ case class Profile(firstName: Option[String], lastName: Option[String]) {
  def fullName: Option[String] = (firstName |@| lastName)(_ + " " + _)
}

@ Profile("A".some, "B".some).fullName 
res2: Option[String] = Some("A B")
@ Profile("A".some, none).fullName 
res3: Option[String] = None
@ Profile(none, "B".some).fullName 
res4: Option[String] = None
@ Profile(none, none).fullName 
res5: Option[String] = None

Upvotes: 0

jub0bs
jub0bs

Reputation: 66432

map2 (see chapter 4 of "the red book") affords you some abstraction:

def map2[A, B, C](oa: Option[A], ob: Option[B])(f: (A, B) => C): Option[C] =
  for {
    a <- oa
    b <- ob
  } yield f(a, b)

Then, leaving the LoginInfo stuff out (because you didn't define profileFor anywhere), you can simply define fullName as

def fullName: Option[String] = map2(firstName, lastName) { _ + " " + _ }

Upvotes: 2

Paweł Jurczenko
Paweł Jurczenko

Reputation: 4471

We can treat Option as a collection and get what you're looking for in a simple one-liner:

val firstName: Option[String] = Some("John")
val lastName: Option[String] = Some("Doe")
val fullName: Option[String] = (firstName ++ lastName).reduceOption(_ + " " + _) // Some("John Doe")

Upvotes: 2

Reactormonk
Reactormonk

Reputation: 21740

I think that's a good application of a for.

case class User(id: UUID, profiles: List[Profile]) {
// Skipped some lines
  def fullName(loginInfo:LoginInfo): Option[String] = for {
    profile <- profileFor(loginInfo)
    first <- profile.firstName
    last <- profile.lastName
  } yield s"$first $last"
}

Upvotes: 3

Synesso
Synesso

Reputation: 39018

Here's one approach

List(firstName, lastName).flatten match {
  case Nil => None
  case xs => Some(xs.mkString(" "))
}

quick testing in the REPL...

scala> def fullName(fn: Option[String], ln: Option[String]): Option[String] = {
     |   List(fn, ln).flatten match {
     |     case Nil => None
     |     case xs => Some(xs.mkString(" "))
     |   }
     | }
fullName: (fn: Option[String], ln: Option[String])Option[String]

scala> fullName(None, None)
res3: Option[String] = None

scala> fullName(Some("a"), None)
res4: Option[String] = Some(a)

scala> fullName(None, Some("b"))
res5: Option[String] = Some(b)

scala> fullName(Some("a"), Some("b"))
res6: Option[String] = Some(a b)

Upvotes: 6

Related Questions