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

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]].


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

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