How to modify this nested case classes with "Seq" fields?
Asked Answered
M

3

8

Some nested case classes and the field addresses is a 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, ...)

I have an employee:

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

It has 3 addresses.

And I want to capitalize the streets start with "b" only. My code is mess like following:

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

The modified employee is then:

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

I'm looking for a way to improve it, and can't find one. Even tried Monocle, but can't apply it to this problem.

Is there any way to make it better?


PS: there are two key requirements:

  1. use only immutable data
  2. don't lose other existing fields
Monody answered 21/10, 2015 at 13:54 Comment(0)
T
15

As Peter Neyens points out, Shapeless's SYB works really nicely here, but it will modify all Street values in the tree, which may not always be what you want. If you need more control over the path, Monocle can help:

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
  }

As Julien Truffaut points out in an edit, you can make this even more concise (but less general) by creating a lens all the way to the first character of the street name:

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
}

There are symbolic operators that would make the definitions above a little more concise, but I prefer the non-symbolic versions.

And then (with the result reformatted for clarity):

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

Note that as in the Shapeless answer, you'll need to change your Employee definition to use List instead of Seq, or if you don't want to change your model, you could build that transformation into the Lens with an Iso[Seq[A], List[A]].

Trilobite answered 21/10, 2015 at 15:10 Comment(0)
K
8

If you are open to replacing the addresses in Company from Seq to List, you can use "Scrap Your Boilerplate" from shapeless (example).

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

You can create a polymorphic function which capitalizes the name of a Street if the name starts with a "b".

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

Which you can use as :

val afterCapitalize = everywhere(capitalizeStreet)(employee)
// Employee(Company(List(
//   Address(Street(aaa street)), 
//   Address(Street(Bbb street)), 
//   Address(Street(Bpp street)))))
Knick answered 21/10, 2015 at 14:58 Comment(2)
Thanks so much!!! That's really cool. I finally got a chance to know how powerful shapeless is!Monody
Nice answer, but see mine for a warning (this will transform any street names in the data structure).Trilobite
A
2

Take a look at quicklens

You could do it like this

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
    }
  }
}
Abdicate answered 22/10, 2015 at 2:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.