case class Person( name: String, address: Address)
case class Address( street: Street, zip: Int, city: String)
case class Street( number: Int, name: String)
val pat = Person( name = "pat", address = Address( street = Street( number = 100, name = "main st"), zip = 12345, city = "anytown"))
val pat = Person( name = "pat", address = Address( street = Street( number = 100, name = "main st"), zip = 12345, city = "anytown"))
new street number: 101
updating street number
pattern matching
pat match { case Person(name,Address(Street(number,stName),zip,city)) => Person(name,Address(Street(number+1,stName),zip,city)) }
repetitive
room for mistakes
name clashes
copying
pat.copy( address = pat.address.copy( street = pat.address.street.copy( number = pat.address.street.number + 1)))
repetitive
room for mistakes
val foo = Foo(bar = bar, baz = baz) foo.copy(bar = newBar) // => Foo(newBar, baz)
modifier functionsdef modifyPersonAddressStreetNumber( p: Person, f: Int => Int): Person = p.copy( address = p.address.copy( street = pat.address.street.copy( number = f(pat.address.street.number))))
// setter modifyPersonAddressStreetNumber(pat, _ => 101)
// modifier modifyPersonAddressStreetNumber(pat, _ + 1)
getter functionsmeet Jean
val jean = Person( "jean", Address( Street(123, "main st"), 12345, "anytown"))
Jean lives down the street
getter functions
val couple = Couple(pat, jean)
case class Couple( person1: Person, person2: Person)
Pat and Jean are a couple
…and Jean is moving in
getter functions
how do we get Pat’s street number
so that we can update Jean’s?
(for example’s sake, ignore the fact that we actually only need to get/set the address)
getter functionsdef getStreetNumber(p: Person): Int = p.address.street.number
why bother?
to avoid violating Law of Demeter!
but - we can do better.
but we don’t just want things to compose
we want things to compose associatively
(Lens[A,B] x Lens[B,C]) x Lens[C,D] ==
Lens[A,B] x (Lens[B,C] x Lens[C,D])
getter functionsour getter from before
def getStreetNumber(p: Person): Int = p.address.street.number
can be decomposed into several getters((_:Person).address) andThen (_.street) andThen (_.number)
functions compose associatively! (technically untrue, since Scala needs hand-holding when it comes to type inference, sadly)
modifier functionsdef modifyPersonAddressStreetNumber( p: Person, f: Int => Int): Person = p.copy( address = p.address.copy( street = pat.address.street.copy( number = f(pat.address.street.number))))
our modifier from before
can be decomposed into several modifiers
def modifyPersonAddress(p: Person, f: Address => Address): Person = p.copy(address = f(p.address))
def modifyAddressStreet(a: Address, f: Street => Street): Address = a.copy(street = f(a.street))
def modifyStreetNumber(s: Street, f: Int => Int): Street = s.copy(number = f(s.number))
modifier functionscan we get the same sort of associative
composability as the getters?
yes we can!def modifierCompose[A,B,C]( modFn1: (A, B => B) => A, modFn2: (B, C => C) => B): (A, C => C) => A = { (a: A, f: C => C) => modFn1(a, b => modFn2(b, f)) }
modifierCompose( modifierCompose(modifyPersonAddress, modifyAddressStreet), modifyStreetNumber)
modifierCompose( modifyPersonAddress, modifierCompose(modifyAddressStreet, modifyStreetNumber))
==
mo’ better
case class Lens[A,B]( get: A => B, mod: A => (B => B) => A) { def compose[C](other: Lens[B,C]): Lens[A,C] = Lens[A,C]( get = get andThen other.get, mod = a => f => mod(a)(b => other.mod(b)(f))) }
we define a Lens class like so
mo’ betterand define instances like so
val PersonToAddress = Lens[Person, Address]( get = _.address, mod = x => f => x.copy(address = f(x.address)))
val AddressToStreet = Lens[Address, Street]( get = _.street, mod = x => f => x.copy(street = f(x.street)))
val StreetToNumber = Lens[Street, Int]( get = _.number, mod = x => f => x.copy(number = f(x.number)))
(this can be done mechanically by a compiler plugin)
mo’ betterand voila
val patsHouse = PersonToAddress compose AddressToStreet compose StreetToNumber get pat
val happyJean = (PersonToAddress compose AddressToStreet compose StreetToNumber ).mod (jean) (_ => patsHouse)
happyJean shouldBe Person("jean", Address(Street(100, "main st"), 12345, "anytown"))
drawbacks to lensesnew concepts
requires up-front investment in learningmight not be worth it depending on
your situation
and, finally, back to our story…we were able to define our lenses
which allowed us to update Jean’s addresswhich allowed Jean to move in with Pat
and the two lived happily ever after
more• we’ve only looked at naive lenses
• van Laarhoven lenses are more general
• effectful updates
• polymorphic updates
• phantom fields
• other optics: prisms, isos, etc.