Объединить коллекцию линз

Monocle - это отличная библиотека (и не единственная), которая реализует шаблон линз, что замечательно, если нам нужно изменить одно поле в огромном вложенном объекте. Как в примере http://julien-truffaut.github.io/Monocle/

case class Street(number: Int, name: String)
case class Address(city: String, street: Street)
case class Company(name: String, address: Address)
case class Employee(name: String, company: Company)

Следующий шаблон

employee.copy(
  company = employee.company.copy(
    address = employee.company.address.copy(
      street = employee.company.address.street.copy(
        name = employee.company.address.street.name.capitalize // luckily capitalize exists
      )
    )
  )
)

Может быть легко заменено на

import monocle.macros.syntax.lens._

employee
  .lens(_.company.address.street.name)
  .composeOptional(headOption)
  .modify(_.toUpper)

Что здорово. Насколько я понимаю, макросы магии преобразуют все точно в тот же код, что и выше.

Однако что, если я хочу совместить несколько действий? Что если я хочу изменить название улицы, адрес города и название компании одновременно одним звонком? Как следующее:

employee.copy(
  company = employee.company.copy(
    address = employee.company.address.copy(
      street = employee.company.address.street.copy(
        name = employee.company.address.street.name.capitalize // luckily capitalize exists
      ),
      city = employee.company.address.city.capitalize
    ),
    name = employee.company.name.capitalize
  )
)

Если бы я просто использовал здесь линзы, у меня был бы следующий код:

employee
  .lens(_.company.address.street.name).composeOptional(headOption).modify(_.toUpper)
  .lens(_.company.address.city).composeOptional(headOption).modify(_.toUpper)
  .lens(_.company.name).composeOptional(headOption).modify(_.toUpper)

Который в конечном итоге будет переведен на три employee.copy(...).copy(...).copy(...) вызовы, а не только один employee.copy(...), Как сделать это лучше?

Кроме того, было бы здорово применить последовательность операций. Как последовательность пар Seq[(Lens[Employee, String], String => String)] где первый элемент - это линза, указывающая на правильное поле, а второй - это функция, которая его модифицирует. Это поможет выстроить такую ​​последовательность операций извне. Для приведенного выше примера:

val operations = Seq(
  GenLens[Employee](_.company.address.street.name) -> {s: String => s.capitalize},
  GenLens[Employee](_.company.address.city) -> {s: String => s.capitalize},
  GenLens[Employee](_.company.name) -> {s: String => s.capitalize}
)

или что-то подобное...

1 ответ

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

Это не так.

Этот простой код:

employee.lens(_.name)
  .modify(_.capitalize)

Становится чем-то похожим на это чудовище *:

monocle.syntax.ApplyLens(employee,
    new monocle.PLens[Employee, Employee, String, String] {
      def get(e: Employee): String = e.name;
      def set(s: String): Employee => Employee = _.copy(s);
      def modify(f: String => String): Employee => Employee = e => e.copy(f(e.name))
    }
}).modify(_.capitalize)

Что довольно далеко от простого

employee.copy(name = employee.name.capitalize)

и включает в себя три избыточных объекта (анонимный класс линз, ApplyLens для синтаксического сахара и лямбда-выражения, возвращаемые из modify). И мы пропустили больше, используя capitalize непосредственно вместо того, чтобы сочинять с headOption,

Так что нет, нет бесплатного обеда. Однако в большинстве случаев это достаточно хорошо, и никто не заботится о дополнительных объектах объектива и промежуточных результатах.


Несколько операций

Вы можете создать Traversal (коллекционную линзу) из нескольких линз, если их типы совпадают (здесь это Employee в String)

val capitalizeAllFields = Traversal.applyN(
  GenLens[Employee](_.name),
  GenLens[Employee](_.company.address.street.name),
  GenLens[Employee](_.company.address.city),
  GenLens[Employee](_.company.name)
).modify(_.capitalize)

Это все равно позвонит copy многократно. Для эффективности вы можете использовать Traversal.apply4 и другие. сорта, которые потребуют от вас написать, что copy вручную (и мне сейчас лень это делать).

Наконец, если вы хотите применить различные преобразования к различным типам полей, вы должны использовать тот факт, что modify а также set вернуть функцию типа Employee => Employee, Для вашего примера это будет:

val operations = Seq(
  GenLens[Employee](_.company.address.street.name).modify(_.capitalize),
  GenLens[Employee](_.company.address.street.number).modify(_ + 42),
  GenLens[Employee](_.company.name).set("No Company Inc.")
)

val modifyAll = Function.chain(operations)

// does all above operations of course, with two extra copy calls
modifyAll(employee) 

* - это упрощенный вывод desugar в Аммонит-Репл. Я пропустил modifyFкстати

Другие вопросы по тегам