Как изменить эти вложенные классы case с полями Seq?

Некоторые вложенные классы case и поле addresses равно Seq[Address]:

// ... means other fields
case class Street(name: String, ...)
case class Address(street: Street, ...)
case class Company(addresses: Seq[Address], ...)
case class Employee(company: Company, ...)

У меня есть сотрудник:

val employee = Employee(Company(Seq(
    Address(Street("aaa street")),
    Address(Street("bbb street")),
    Address(Street("bpp street")))))

У него 3 адреса.

И я хочу, чтобы улицы начинались только с буквы «б». Мой код беспорядок, как показано ниже:

val modified = employee.copy(company = employee.company.copy(addresses = 
    employee.company.addresses.map { address =>
        address.copy(street = address.street.copy(name = {
          if (address.street.name.startsWith("b")) {
            address.street.name.capitalize
          } else {
            address.street.name
          }
        }))
      }))

Сотрудник modified тогда:

Employee(Company(List(
    Address(Street(aaa street)), 
    Address(Street(Bbb street)), 
    Address(Street(Bpp street)))))

Я ищу способ улучшить его, и не могу его найти. Даже попробовал Monocle, но не смог применить его к этой проблеме.

Есть ли способ сделать это лучше?


PS: есть два ключевых требования:

  1. использовать только неизменяемые данные
  2. не потерять другие существующие поля

person Freewind    schedule 21.10.2015    source источник


Ответы (3)


Как отмечает Питер Нейенс, SYB Shapeless работает здесь очень хорошо, но он изменит все Street значения в дереве, что не всегда может быть тем, что вам нужно. Если вам нужно больше контроля над путем, Monocle может помочь:

import monocle.Traversal
import monocle.function.all._, monocle.macros._, monocle.std.list._

val employeeStreetNameLens: Traversal[Employee, String] =
  GenLens[Employee](_.company).composeTraversal(
    GenLens[Company](_.addresses)
      .composeTraversal(each)
      .composeLens(GenLens[Address](_.street))
      .composeLens(GenLens[Street](_.name))
  )

  val capitalizer = employeeStreeNameLens.modify {
    case s if s.startsWith("b") => s.capitalize
    case s => s
  }

Как отмечает Жюльен Трюффо в редактировании, вы можете сделать это еще более кратким (но менее общим), создав линзу до первого символа названия улицы:

import monocle.std.string._

val employeeStreetNameFirstLens: Traversal[Employee, Char] =
  GenLens[Employee](_.company.addresses)
    .composeTraversal(each)
    .composeLens(GenLens[Address](_.street.name))
    .composeOptional(headOption)

val capitalizer = employeeStreetNameFirstLens.modify {
  case 'b' => 'B'
  case s   => s
}

Существуют символические операторы, которые сделали бы приведенные выше определения немного более краткими, но я предпочитаю несимволические версии.

А затем (с переформатированным результатом для ясности):

scala> capitalizer(employee)
res3: Employee = Employee(
  Company(
    List(
      Address(Street(aaa street)),
      Address(Street(Bbb street)),
      Address(Street(Bpp street))
    )
  )
)

Обратите внимание, что, как и в случае с бесформенным ответом, вам нужно изменить определение Employee, чтобы использовать List вместо Seq, или, если вы не хотите менять свою модель, вы можете встроить это преобразование в Lens с Iso[Seq[A], List[A]].

person Travis Brown    schedule 21.10.2015

Если вы готовы заменить addresses в Company с Seq на List, вы можете использовать "Scrap Your Boilerplate" из shapeless (пример).

import shapeless._, poly._

case class Street(name: String)
case class Address(street: Street)
case class Company(addresses: List[Address])
case class Employee(company: Company)

val employee = Employee(Company(List(
    Address(Street("aaa street")),
    Address(Street("bbb street")),
    Address(Street("bpp street")))))

Вы можете создать полиморфную функцию, которая использует заглавную букву имени Street, если имя начинается с "b".

object capitalizeStreet extends ->(
  (s: Street) => {
    val name = if (s.name.startsWith("b")) s.name.capitalize else s.name
    Street(name)
  }
)

Который вы можете использовать как:

val afterCapitalize = everywhere(capitalizeStreet)(employee)
// Employee(Company(List(
//   Address(Street(aaa street)), 
//   Address(Street(Bbb street)), 
//   Address(Street(Bpp street)))))
person Peter Neyens    schedule 21.10.2015
comment
Огромное спасибо!!! Это действительно круто. Наконец-то я получил шанс узнать, насколько могуществен бесформенный! - person Freewind; 21.10.2015
comment
Хороший ответ, но см. мой для предупреждения (это изменит любые названия улиц в структуре данных). - person Travis Brown; 21.10.2015

Взгляните на quicklens.

Вы могли бы сделать это так

import com.softwaremill.quicklens._

case class Street(name: String)
case class Address(street: Street)
case class Company(address: Seq[Address])
case class Employee(company: Company)
object Foo {
  def foo(e: Employee) = {
    modify(e)(_.company.address.each.street.name).using {
      case name if name.startsWith("b") => name.capitalize
      case name => name
    }
  }
}
person jilen    schedule 22.10.2015