Как объединить две переменные Option[String] в одну при использовании flatMap в Scala?

У меня есть следующий класс:

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

Теперь я хочу удалить fullName атрибут, потому что это избыточно. Тем не менее, у меня есть метод в моем классе User который возвращает fullName:

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

Сейчас я пытаюсь заменить .flatMap(_.fullName) расстаться с конкатенацией firstName + lastName, Как это может быть сделано? Нужно ли делать новый Option[String], как это:

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

6 ответов

Решение

Я думаю, что это хорошее применение 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"
}

Вот один из подходов

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

быстрое тестирование в 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)

Мы можем лечить Option в виде коллекции и получите то, что вы ищете в простой однострочной:

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

map2 (см. главу 4 " Красной книги") дает вам некоторую абстракцию:

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)

Затем, оставив LoginInfo вещи (потому что вы не определили profileFor где угодно), вы можете просто определить fullName как

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

На самом деле я недавно написал сообщение в блоге с парой нативных опций Scala.

Тот, который мне понравился больше всего похож на ответ @Pawels только с использованием 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)

Этот подход хорош, потому что он работает, когда любой из Option[T] является 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)

Еще одним приятным свойством этого является то, что он может работать и для N элементов при использовании varargs:

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)

Я думаю, что с самого начала fullName в Profile класс, но как метод и из вашего User класс просто делегировать его Profile,

Вы можете использовать любое решение, которое уже дано, я опубликую его, используя скалаз и аппликативный компоновщик, оно похоже map2просто более общий, поскольку вы можете присоединиться к любому (ну, на самом деле, не любому, но разумному количеству, поскольку вам нужно передать функцию, которая принимает то же количество аргументов, что и количество объектов, к которым вы хотите применить), число опций или другое аппликативный.

@ 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
Другие вопросы по тегам