如何修改具有“Seq”字段的嵌套案例类?

8

一些嵌套的 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个地址。

我只想将以“b”开头的街道名称大写。我的代码很混乱,如下所示:

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
          }
        }))
      }))

修改后的员工如下:
Employee(Company(List(
    Address(Street(aaa street)), 
    Address(Street(Bbb street)), 
    Address(Street(Bpp street)))))

我正在寻找改进它的方法,但是找不到。甚至尝试了Monocle,但无法应用到这个问题上。

有没有方法使其更好?


备注:有两个关键要求:

  1. 只使用不可变数据
  2. 不丢失其他现有字段
3个回答

15

正如Peter Neyens所指出的那样,Shapeless的SYB在这里非常有效,但它会修改树中所有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
  }

正如Julien Truffaut在编辑中指出的那样,您可以通过创建一个透镜一直到街道名称的第一个字符来使其更加简洁(但不太通用):

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))
    )
  )
)

请注意,与Shapeless的答案一样,您需要更改Employee定义,以使用List而不是Seq,或者如果您不想更改模型,则可以将该转换构建到Lens中,使用Iso [Seq [A],List [A]]


8

如果你愿意将 Company 中的 addressesSeq 替换为 List,你可以使用 shapeless 的 "Scrap Your Boilerplate"(示例)。

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")))))

您可以创建一个多态函数,如果街道名称以"b"开头,则将Street的名称大写。

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)))))

1
非常感谢!太棒了。我终于有机会知道shapeless有多强大了! - Freewind
3
回答不错,但请注意我的警告(这将转换数据结构中的任何街道名称)。 - Travis Brown

2
请查看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
    }
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接