Объединить коллекцию линз
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
кстати