Lenses and optics
A Lens[M, V] is get + set on an immutable model. derives Optics plus
Optics[M](_.field.nested) builds that lens at compile time from a field-selection path.
No reflection. Unmodified sibling trees keep the same reference, which is what
FastEq withReferenceEquality exploits.
Derive a path
case class Address(city: String, zip: String) derives Optics
case class User(name: String, age: Int, address: Address) derives Optics
case class Model(user: User, count: Int) derives Optics
val city = Optics[Model](_.user.address.city)
The path must be a chain of field selects. Method calls and match are compile errors.
{
val city = Optics[Model](_.user.address.city)
val start = Model(User("Ada", 36, Address("NYC", "10001")), 0)
val moved = city.set(start, "Boston")
(city.get(start), city.get(moved), moved.user.name)
}(NYC,Boston,Ada)Mermoid.diagram(compose)What the macro writes
Optics[Model](_.user.address.city) is the same copy chain you would type by hand:
new Lens[Model, String]:
def get(m: Model) = m.user.address.city
def set(m: Model, v: String) =
m.copy(user = m.user.copy(address = m.user.address.copy(city = v)))
Compose
>> (also compose) turns Lens[A, B] and Lens[B, C] into Lens[A, C]. Nested
Optics[M](_.a.b) is that composition, inlined.
{
val user = Optics[Model](_.user)
val city = Optics[User](_.address.city)
val path = user >> city
val start = Model(User("Ada", 36, Address("NYC", "10001")), 0)
path.get(start)
}NYCMermoid.diagramInteractive(compose, initialWidth = 560)Laws
For a lawful lens: get-set is identity, set-get returns the written value, set-set keeps the last write. Collection helpers in Collection lenses are lawful at every defined index; out-of-range writes are no-ops.