Monocle is a great library (and not the only one) which implements lenses pattern, which is great if we have to change one field in huge nested object. Like in example 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)
The following boilerplate
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
)
)
)
)
Can easily be replaced with
import monocle.macros.syntax.lens._
employee
.lens(_.company.address.street.name)
.composeOptional(headOption)
.modify(_.toUpper)
Which is great. As far as I understand, macros magic converts everything exactly to the same code as above.
However, what if I want to combine several actions? What if I want to change street name, address city and company name at the same time with one call? Like the following:
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
)
)
If I just reuse lenses here, I would have the following code:
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)
Which will eventually be translated to THREE employee.copy(...).copy(...).copy(...)
invocations, not just ONE employee.copy(...)
. How to make it better?
Furthermore, it would be really great to apply a sequence of operations. Like sequence of pairs Seq[(Lens[Employee, String], String => String)]
where first element is a lens pointing to the correct field and the second one is a function which modifies it. It will help to build such sequence of operations from the outside. For the above example:
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}
)
or something similar...
As far as I understand, macros magic converts everything exactly to the same code as above.
It doesn't.
This simple code:
employee.lens(_.name)
.modify(_.capitalize)
Becomes something along the lines of that monstrosity *:
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)
Which is quite far from simple
employee.copy(name = employee.name.capitalize)
and includes three redundant objects (anonymous lens class, ApplyLens for syntax sugar and lambda returned from modify
). And we skipped more by using capitalize
directly instead of composing with headOption
.
So no, there is no free dinner. Most of the time, however, it's good enough and nobody cares about extra lens objects and intermediate results.
You can build a Traversal (collection lens) from multiple lenses if their types align (here it's Employee
to 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)
This will still call copy
multiple times. For efficiency, you can use Traversal.apply4
et al. varieties, which will require you to write that copy
manually (and I'm too lazy to do it now).
Finally, if you want to apply various transformations to different types of fields, you are supposed to use the fact that modify
and set
return a function of type Employee => Employee
. For your example that would be:
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)
* - this is simplified output from desugar
in Ammonite-REPL. I skipped modifyF
, btw
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With