beyond scala lens

67
Beyond Scala Lenses github: @julien-truffaut or twitter: @JulienTruffaut

Upload: julien-truffaut

Post on 15-Jul-2015

2.142 views

Category:

Software


5 download

TRANSCRIPT

Page 1: Beyond Scala Lens

Beyond Scala Lenses github: @julien-truffaut or twitter: @JulienTruffaut

Page 2: Beyond Scala Lens

Function

S A

A function transforms all s in S into an A

Page 3: Beyond Scala Lens

Function

Page 4: Beyond Scala Lens

Isomorphism

S A

For all s: S, g(f(s)) == s For all a: A, f(g(a)) == a

f

g

Page 5: Beyond Scala Lens

Iso case class Iso[S,A]( get : S => A, reverseGet: A => S )

Properties: For all s: S, reverseGet(get(s)) == s For all a: A, get(reverseGet(a)) == a

Page 6: Beyond Scala Lens

Modify

A Af

Page 7: Beyond Scala Lens

Modify

A A

S S

f

get reverseGet

modify

Page 8: Beyond Scala Lens

Compose

S AIso

BIso

Iso

Page 9: Beyond Scala Lens

Iso Derived Methods

case class Iso[S,A]( get : S => A, reverseGet: A => S ){ def modify(m: A => A): S => S def reverse: Iso[A,S] def compose[B](other: Iso[A,B]): Iso[S,B] }

Page 10: Beyond Scala Lens

Units

class Robot{ def moveBy(d: Double): Robot } val nono: Robot = … nono.moveBy(100.5) // Meters nono.moveBy(3) // Kilometers nono.moveBy(-2.5) // Yards

Page 11: Beyond Scala Lens

Safe Unit case class Meter(d: Double) case class Yard(d: Double) class Robot{ def moveBy(m: Meter): Robot } nono.moveBy(Meter(100.5)) nono.moveBy(10) // does not compile nono.moveBy(Yard(3.0)) // does not compile

Page 12: Beyond Scala Lens

Iso

Meter Yard

Page 13: Beyond Scala Lens

Meter to Yard

val meterToYard: Iso[Meter, Yard] = Iso( m => Yard(m.value * 1.09361), y => Meter(y.value / 1.09361) ) meterToYard.get(Meter(200)) == Yard(218.7219999…) nono.moveBy(meterToYard.reverseGet(Yard(2.5))

Page 14: Beyond Scala Lens

Iso

Meter Yard

Kilometer

Page 15: Beyond Scala Lens

Iso

Meter Yard

Kilometer Mile

Page 16: Beyond Scala Lens

Iso Composition case class Kilometer(value: Double) case class Mile(value: Double) val meterToKilometer: Iso[Meter, Kilometer] = … val yardToMile : Iso[Yard, Mile] = … val kilometerToMile: Iso[Kilometer, Mile] = meterToKilometer.reverse compose meterToYard compose yardToMile

Page 17: Beyond Scala Lens

Different Representation

val stringToList: Iso[String, List[Char]] = Iso(_.toList, _.mkString(“”)) def listToVector[A]: Iso[List[A], Vector[A]] = Iso(_.toVector, _.toList)

Page 18: Beyond Scala Lens

Generic Programming

case object Red val red: Iso[Red, Unit] = Iso(Red => (), () => Red) case class Person(name: String, age: Int) val personToTuple: Iso[Person, (String, Int)] = …

Page 19: Beyond Scala Lens

Iso Properties

For all s: S, reverseGet(get(s)) == s For all a: A, get(reverseGet(a)) == a

Page 20: Beyond Scala Lens

Scalacheck def isoLaws[S,A](iso: Iso[S,A]) = new Properties { property(“One Way”) = forAll { s: S => iso.reverseGet(iso.get(s)) == s } property(“Other Way”) = forAll { a: A => iso.get(iso.reverseGet(a)) == a } }

Page 21: Beyond Scala Lens

Scalacheck import org.spec2.scalaz.Spec class IsoSpec extends Spec { checkAll(“meter to yard”, isoLaws(meterToYard)) }

val meterToYard: Iso[Meter, Yard] = Iso( m => Yard(m.value * 1.09361), y => Meter(y.value / 1.09361) )

Page 22: Beyond Scala Lens

Scalacheck

A counter-example is: [Yard(1.518707721E9)] (after 24 tries) scala> meterToYard.get(meterToYard.reverseGet( Yard(1.518707721E9))) scala> res0: Yard= Yard(1.5187077210000002E9)

Page 23: Beyond Scala Lens

Relaxed Isomorphism

S A

For all s: S such as f(s) exists, g(f(s)) == s For all a: A, f(g(a)) == a

?

f

g

f is a Function[S, Option[A]] g is a Function[A, S]

Page 24: Beyond Scala Lens

Prism case class Prism[S,A]( getOption : S => Option[A], reverseGet: A => S )

Properties: For all s: S, getOption(s) map reverseGet == Some(s) || None For all a: A, getOption(reverseGet(a)) == Some(a)

Page 25: Beyond Scala Lens

Pattern matching / Constructor

sealed trait List[A] case class Cons[A](h: A, t: List[A]) extends List[A] case class Nil[A]() extends List[A] Cons.unapply(List(1,2,3)) == Some((1, List(2,3))) Cons.unapply(Nil) == None Cons.apply(1, List(2,3)) == List(1,2,3)

Page 26: Beyond Scala Lens

Prism sealed trait List[A] case class Cons[A](h: A, t: List[A]) extends List[A] case class Nil[A]() extends List[A] def cons[A]: Prism[List[A], (A, List[A])] = … cons.getOption(List(1,2,3)) == Some((1, List(2,3))) cons.getOption(Nil) == None cons.reverseGet(1, List(2,3)) == List(1,2,3)

Page 27: Beyond Scala Lens

Prism Derived Methods case class Prism[S,A]( getOption: S => Option[A], reverseGet: A => S ){ def isMatching(s: S): Boolean def modify(f: A => A): S=> S def modifyOption(f: A => A): S => Option[S] def compose[B](other: Prism[A,B]): Prism[S,B] def compose[B](other: Iso[A,B]): ???[S,B] }

Page 28: Beyond Scala Lens

Iso – Prism Optic f g

Iso S => A A => S Prism S => Option[A] A => S

def isoToPrism[S,A](iso: Iso[S,A]): Prism[S,A] = Prism( getOption = s => Some(iso.get(s)), reverseGet = iso.reverseGet )

Page 29: Beyond Scala Lens

Iso – Prism case class Prism[S,A]{ def compose[B](other: Prism[A,B]): Prism[S,B] def compose[B](other: Iso[A,B]): Prism[S,B] } case class Iso[S,A]{ def compose[B](other: Iso[A,B]): Iso[S,B] def compose[B](other: Prism[A,B]): Prism[S,B] }

Page 30: Beyond Scala Lens

Enum sealed trait Day case object Monday extends Day case object Tuesday extends Day val tuesday: Prism[Day, Unit] = … tuesday.getOption(Monday) == None tuesday.getOption(Tuesday) == Some(()) tuesday.reverseGet(()) == Tuesday

Page 31: Beyond Scala Lens

Json sealed trait Json case class JNumber(v: Double) extends Json case class JString(s: String) extends Json val jNum: Prism[Json, Double] = … jNum.modify(_ + 1)(JNumber(2.0)) == JNumber(3.0) jNum.modify(_ + 1)(JString(“”)) == JString(“”) jNum.modifyOption(_ + 1)(JString(“”)) == None

Page 32: Beyond Scala Lens

Safe Down Casting

val doubleToInt: Prism[Double, Int] = … doubleToInt.getOption(3.4) == None doubleToInt.getOption(3.0) == Some(3) doubleToInt.reverseGet(5) == 5.0

Page 33: Beyond Scala Lens

Prism Composition sealed trait Json case class JNumber(v: Double) extends Json case class JString(s: String) extends Json val jInt: Prism[Json, Int] = jNum compose doubleToInt jInt.getOption(JNumber(3.0)) == Some(3) jInt.getOption(JNumber(5.9)) == None jInt.getOption(JString(“”)) == None

Page 34: Beyond Scala Lens

Where is the bug? def stringToInt: Prism[String, Int] = Prism( getOption = s => Try(s.toInt).toOption, reverseGet = _.toString ) stringToInt.modify(_ * 2)(“5”) == “10” stringToInt.getOption(“5”) == Some(5) stringToInt.getOption(“-3”) == Some(-3) stringToInt.getOption(“5.7”) == None stringToInt.getOption(“99999999999999999”) == None stringToInt.getOption(“Hello”) == None

Page 35: Beyond Scala Lens

Tadam !

” .toInt = 9 “

Page 36: Beyond Scala Lens

Prism

S A B C Or Or

sealed trait S class A extends S class B extends S class C extends S

Page 37: Beyond Scala Lens

Lens

S A B C And And

case class S(a: A, b: B, c: C)

Page 38: Beyond Scala Lens

Lens case class Lens[S,A]( get: S => A, set:(A, S) => S )

Properties: For all s: S, set(get(s), s) == s For all a: A, s: S, get(set(a, s)) == a

Page 39: Beyond Scala Lens

Iso – Lens Optic f g

Iso S => A A => S Lens S => A (A, S) => S

def isoToLens[S,A](iso: Iso[S,A]): Lens[S,A] = Lens( get = iso.get, set = (a, _) => iso.reverseGet(a) )

Page 40: Beyond Scala Lens

Accessors

case class Person(name: String, age: Int) val age = Lens[Person, Int](_.age, (a, p) => p.copy(age = a)) val zoe = Person(“Zoe”, 25) age.get(zoe) == 25 age.set(20, zoe) == Person(“Zoe”, 20) age.modify(_ + 1)(zoe) == Person(“Zoe”, 26)

Page 41: Beyond Scala Lens

Nested Accessors case class Person(name: String, age: Int, address: Address) case class Address(streetNumber: Int, StreetName: String) val address: Lens[Person, Address] = … val streetName: Lens[Address, String] = … val zoe = Person(“Zoe”, 25, Address(10, “High Street”)) (address compose streetName).get(zoe) == “High Street” (address compose streetName).set(“Iffley Road”, zoe) == Person(“Zoe”, 25, Address(10, “Iffley Road”))

Page 42: Beyond Scala Lens

Tuples

def first[A,B]: Lens[(A,B), A] = … def second[A,B]: Lens[(A,B), B] = … val tuple = (2,“Zoe”) first.set(4, tuple) == (4,“Zoe”) second.modify(_.reverse)(tuple) == (2,“eoZ”)

Page 43: Beyond Scala Lens

Universal Case Class Accessors case class Person(name: String, age: Int) val personToTuple: Iso[Person,(String,Int)] = … val zoe = Person(“Zoe”, 25) (personToTuple compose second).set(30, zoe) == Person(“Zoe”, 30)

Page 44: Beyond Scala Lens

At

def at[K,V](k: K): Lens[Map[K,V], Option[V]] = Lens( get = m => m.get(k), set = (optV, m) => optV match { case None => m – k case Some(v)=> m + (k -> v) } )

Page 45: Beyond Scala Lens

Map 1 “one”

2 “two”

1 “one”

at(“three”).set(Some(3), m) 1 “one”

2 “two”

at(“two”).set(None, m)

3 “three”

Page 46: Beyond Scala Lens

What Next?

Optic f g Iso S => A A => S

Prism S => Option[A] A => S Lens S => A (A, S) => S

Page 47: Beyond Scala Lens

Optional

Optic f g Iso S => A A => S

Prism S => Option[A] A => S Lens S => A (A, S) => S

Optional S => Option[A] (A, S) => S

Page 48: Beyond Scala Lens

Optional

Prism Lens

Iso

Page 49: Beyond Scala Lens

Optional case class Optional[S,A]( getOption: S => Option[A], set :(A, S) => S )

Properties: For all s: S, getOption(s) map set(_, s) == Some(s) For all a: A, s: S, getOption(set(a, s)) == Some(a) || None

Page 50: Beyond Scala Lens

Head

def cons[A]: Prism[List[A], (A, List[A])] = … def first[A, B]: Lens[(A, B), A] = … def head[A]: Optional[List[A], A] = cons compose first head.getOption(List(1,2,3)) = Some(1) head.set(0, List(1,2,3)) = List(0,2,3)

Page 51: Beyond Scala Lens

Void def void[S,A]: Optional[S, A] = Optional( getOption = s => None, set = (a, s) => s ) void.getOption(“Hello”) = None void.set(1,“Hello”) = “Hello” void.setOption(1,“Hello”) = None

Page 52: Beyond Scala Lens

Index def index[A](i: Int): Optional[List[A], A] = if(i < 0) void else if(i == 0) head else cons compose second compose index(i – 1) index(-1).getOption(List(1,2,3)) == None index(1).getOption(List(1,2,3)) == Some(2) index(5).getOption(List(1,2,3)) == None index(1).set(10, List(1,2,3)) == List(1,10,3)

Page 53: Beyond Scala Lens

Index for Vector

def vectorToList[A]: Iso[Vector[A], List[A]] = … def indexV(i: Int): Optional[Vector[A], A] = vectorToList compose index(i)

Page 54: Beyond Scala Lens

Index != At 3 5 6 2

0 1 2 3

3 5 99 2

0 1 2 3

index(2).set(99, l)

val l = List(3,5,6,2)

3 5 6 2 ? ? ? 99

0 1 2 3 4 5 6 7

3 5 6 2

0 1 2 3

index(7).set(99, l)

Page 55: Beyond Scala Lens

Study Case: Http Request sealed trait Method case object GET extends Method case object POST extends Method case class URI( host: String, port: Int, path: String, query: Map[String, String] ) case class Request( method: Method, uri: URI, headers: Map[String, String], body: String )

Page 56: Beyond Scala Lens

Study Case: Http Request

val r = Request( method = GET, uri = URI(“localhost”,8080,“/ping”,Map(“age”->“15”)), headers = Map.empty, body = “” )

Page 57: Beyond Scala Lens

Which method?

val method: Lens[Request, Method] = … val GET: Prism[Method, Unit] = … method.get(r) // GET (method compose GET).isMatching(r) // true

Page 58: Beyond Scala Lens

How to update host? val uri: Lens[Request, URI] = … val host: Lens[URI, String] = … (uri compose host).set(“foo.io”)(r) Request( method = GET, uri = URI(“foo.io”,8080,“/ping”,Map(“age”->“15”)), headers = Map.empty, body = “” )

Page 59: Beyond Scala Lens

How to increase age query? val query: Lens[URI, Map[String, String]] = … (uri compose query compose index(“age”) compose stringToInt ).modify(_ + 1)(r) Request( method = GET, uri = URI(“localhost”,8080,“/ping”,Map(“age”->“16”)), headers = Map.empty, body = “” )

Page 60: Beyond Scala Lens

How to add header? val headers: Lens[Request, Map[String, String]] = … (headers compose at(“Content-Length”)).set(Some(“0”))(r) Request( method = GET, uri = URI(“localhost”,8080,“/ping”,Map(“age”->“15”)), headers = Map(“Content-Length”->“0”), body = “” )

Page 61: Beyond Scala Lens

More power!!!

val r2 = Request( method = POST, uri = URI(“localhost”,8080,“/pong”,Map.empty), headers = Map(“x-custom1”->“5”, “x-custom2”->“10”), body = “Hello World” )

Page 62: Beyond Scala Lens

Traversal (headers compose filterIndex(_.startsWith(“x-”)) compose stringToInt ).modify(_ * 2)(r2) Request( method = POST, uri = URI(“localhost”,8080,“/pong”,Map.empty), headers = Map(“x-custom1”->“10”, “x-custom2”->“20”), body = “Hello World” )

Page 63: Beyond Scala Lens

Optional

Prism Lens

Iso

Traversal

Setter

Getter

Fold

Page 64: Beyond Scala Lens

Monocle goodies

▪ Provides lots of built-in optics and functions ▪ Macros for creating Lenses, Iso and Prism ▪ Syntax to use optics as infix operator ▪ Experimental state support

Page 65: Beyond Scala Lens

Resources ▪ Monocle on github

▪ Simon Peyton Jones’s lens talk at Scala Exchange 2013

▪ Edward Kmett on Lenses with the State Monad

Page 66: Beyond Scala Lens

Acknowledgement

▪ Member Monocle and Cats gitter channel for advice and review

▪ Special thanks to Ilan Godik (@NightRa) for helping with slides and content

Page 67: Beyond Scala Lens

Thank you!