52
Intro to Lenses Andrew Mohrland @cxfreeio

Introduction to Lenses

Embed Size (px)

Citation preview

Intro to LensesAndrew Mohrland

@cxfreeio

today

• what

• why

• how

• when

what problems do lenses solve?

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

mutate it!

pat.address.street.number += 1

Just kidding…

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)

“remove repetition by defining a function!”

good idea.

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)

modifier functions

better - but still lacking!

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.

some nested data structure

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’ betterbut this is kind of ugly so far

can we define these things in a cleaner way?

yup

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"))

mo’ better

there we have it

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.

thanks