Scala Объект Option внутри другого объекта Option
У меня есть модель, которая имеет несколько полей Option, которые содержат другие поля Option. Например:
case class First(second: Option[Second], name: Option[String])
case class Second(third: Option[Third], title: Option[String])
case class Third(numberOfSmth: Option[Int])
Я получаю эти данные из внешних JSON, и иногда эти данные могут содержать нулевые значения, что было причиной такого дизайна модели.
Итак, вопрос в том, как лучше всего получить самое глубокое поле?
First.get.second.get.third.get.numberOfSmth.get
Вышеуказанный метод выглядит действительно некрасиво и может вызвать исключение, если один из объектов будет None. Я искал в Scalaz lib, но не нашел лучшего способа сделать это.
Есть идеи? Заранее спасибо.
4 ответа
Решение заключается в использовании Option.map
а также Option.flatMap
:
First.flatMap(_.second.flatMap(_.third.map(_.numberOfSmth)))
Или эквивалент (см. Обновление в конце этого ответа):
First flatMap(_.second) flatMap(_.third) map(_.numberOfSmth)
Это возвращает Option[Int]
(при условии, что numberOfSmth
возвращает Int
). Если какой-либо из параметров в цепочке вызовов None
результат будет None
иначе будет Some(count)
где count
это значение, возвращаемое numberOfSmth
,
Конечно, это может быть очень быстро. По этой причине scala поддерживает понимание как синтаксический сахар. Выше можно переписать как:
for {
first <- First
second <- first .second
third <- second.third
} third.numberOfSmth
Что, возможно, лучше (особенно если вы еще не привыкли видеть map
/flatMap
везде, что, безусловно, будет иметь место через некоторое время с использованием Scala), и генерирует точно такой же код под капотом.
Для получения дополнительной информации, вы можете проверить этот другой вопрос: Какова доходность Scala?
ОБНОВЛЕНИЕ: Спасибо Бену Джеймсу за то, что он указал, что flatMap ассоциативен. Другими словами x flatMap(y flatMap z)))
такой же как x flatMap y flatMap z
, Хотя последний, как правило, не короче, он имеет преимущество в том, что избегает вложений, за которыми легче следить.
Вот некоторая иллюстрация в REPL (4 стиля эквивалентны, первые два используют вложение flatMap, остальные два - плоские цепочки flatMap):
scala> val l = Some(1,Some(2,Some(3,"aze")))
l: Some[(Int, Some[(Int, Some[(Int, String)])])] = Some((1,Some((2,Some((3,aze))))))
scala> l.flatMap(_._2.flatMap(_._2.map(_._2)))
res22: Option[String] = Some(aze)
scala> l flatMap(_._2 flatMap(_._2 map(_._2)))
res23: Option[String] = Some(aze)
scala> l flatMap(_._2) flatMap(_._2) map(_._2)
res24: Option[String] = Some(aze)
scala> l.flatMap(_._2).flatMap(_._2).map(_._2)
res25: Option[String] = Some(aze)
Нет необходимости в скалязе:
for {
first <- yourFirst
second <- f.second
third <- second.third
number <- third.numberOfSmth
} yield number
В качестве альтернативы вы можете использовать вложенные flatMaps
Это может быть сделано путем объединения вызовов flatMap
:
def getN(first: Option[First]): Option[Int] =
first flatMap (_.second) flatMap (_.third) flatMap (_.numberOfSmth)
Вы также можете сделать это для простоты понимания, но оно более многословно, так как заставляет вас называть каждое промежуточное значение:
def getN(first: Option[First]): Option[Int] =
for {
f <- first
s <- f.second
t <- s.third
n <- t.numberOfSmth
} yield n
Я думаю, что это излишне для вашей проблемы, но просто для справки:
Эта проблема с вложенным доступом решается с помощью концепции, называемой линзами. Они предоставляют удобный механизм доступа к вложенным типам данных с помощью простой композиции. В качестве введения вы, возможно, захотите проверить, например, этот SO-ответ или это руководство. Вопрос о том, имеет ли смысл использовать линзы в вашем случае, заключается в том, нужно ли вам также выполнять много обновлений в структуре вложенных опций (примечание: обновление не в изменяемом смысле, а возвращает новый измененный, но неизменный экземпляр). Без линз это приводит к длительному вложенному классу copy
код. Если вам вообще не нужно обновляться, я бы придерживался предложения om-nom-nom.