Optics with Monocle
Modeling the part and the whole
By Ilan Godik (@Nightra on IRC)
About me
▪ A CS Undergraduate at Haifa University
▪ A contributor to the Monocle library
▪ Functional programming lover.
What are lenses?
trait Lens[S,A]{
def get(s: S): A
def set(a: A)(s: S): S
}
In the simplest model, it's just a pair of a getter and a setter:
get
S
A1
S
A2
set
Lens[S, A]
A2
Classic lens example
person.copy(
address = person.address.copy(
street = person.address.street.copy(
name = person.address.street.name.capitalize
)
)
)
Updating nested structures is verbose and painful
case class Person(fullName: String, address: Address)
case class Address(city: String, street: Street)
case class Street(name: String, number: Int)
Monocle Lenses
(address composeLens street composeLens name).modify(_.capitalize)(person)
case class Person(fullName: String, address: Address)
case class Address(city: String, street: Street)
case class Street(name: String, number: Int)
val address = Lenser[Person](_.address)
val street = Lenser[Address](_.street)
val name = Lenser[Street](_.name)
Define lenses once, and compose as you wish
Lenses Macro annotation
(address composeLens street composeLens name).modify(_.capitalize)(person)
@Lenses case class Person(fullName: String, address: Address)
@Lenses case class Address(city: String, street: Street)
@Lenses case class Street(name: String, number: Int)
import Person._, Address._, Street._
Awesome, but not IDE friendly.
The object graph
Person
String Address
String Street
String Int
Code size growth
▪ Vanilla Scala: O(h2)
▪ Lenses: O(h)
Where h is the height of the object graph
Composition: Follow the arrows
Person
String Address
String Street
String Int
Lens composition
We compose lenses exactly how we would do it with copy.
S
set
Lens[S,A] compose Lens[A,B]
A
B2
get
S
A
B1
get B2
Lens composition
We compose lenses exactly how we would do it with copy.
S
set
Lens[S,A] compose Lens[A,B]
A
B2
get
S
A
B1
get B2
Lens composition
We compose lenses exactly how we would do it with copy.
S
set
Lens[S,A] compose Lens[A,B]
A
B2
get
S
A
B1
get B2
A
B2
Lens composition
We compose lenses exactly how we would do it with copy.
S
set
Lens[S,A] compose Lens[A,B]
A
B2
get
S
A
B1
get B2
A
B2
Polymorphic Lenses
Can we change the type of part of the structure while setting?
first.set("Hello")((1,2)) == ("Hello",2)
Polymorphic Lenses
Can we change the type of part of the structure while setting?
trait PLens[S, T, A, B] {
def get(s: S): A
def set(b: B)(s: S): T
} get
S
A
T
B
B
set
PLens[S, T, A, B]
Example: Unit conversion
Meters CM MM
?
Lens[Meters,CM]
Lens[Meters,MM]
Optics as different points of view of data
Isomorphisms
Do we get more power if our lens is bidirectional?
Meters CM MM
Iso[Meters,CM]
Iso[Meters,MM]
Isomorphisms
A B
Isomorphisms as a bijection: A function with an inverse
Prisms
What if we don’t have such a nice correspondence?
?
?
A B
Prisms
What if we don’t have such a nice correspondence?
?
?
IntString
A B
Prisms
What if we don’t have such a nice correspondence?
?
?
IntString
IntString
Option[Int]String
A B
Prisms
What if we don’t have such a nice correspondence?
?
?
IntString
IntString
Option[Int]String
Laws:
1. If there is an answer, going
back must give the source.
2. If we go back, there must
be an answer, which is
the source.
A B
Property Testing
Laws => Automated property testing
Int =String
Option[Int] =String
_.toString
Try(_.toInt).toOption
Property Testing
” .toInt = 9“
Property Testing
” .toInt = 9“
WAT
Prism Laws
?
IntString
A B
“9”
9
Example: Double binding
°C30 °F86
Iso[Celcius, Fahrenheit]
Prism[String, °F]Prism[String, °C]
Example: Double binding
°C30 °F86
Iso[Celcius, Fahrenheit]
Prism[String, °F]Prism[String, °C]
Example: Double binding
°C30 °F86
Iso[Celcius, Fahrenheit]
Prism[String, °F]Prism[String, °C]
A bit of theory:
Van Laarhoven Lenses
Is it possible to unify all the lens functions?
▪ get: S => A
▪ set: A => S => S
▪ modify: (A => A) => (S => S)
Is it possible to unify all the lens functions?
▪ get: S => A
▪ set: A => S => S
▪ modify: (A => A) => (S => S)
▪ modifyMaybe: (A => Option[A]) => (S => Option[S])
▪ modifyList: (A => List[A]) => (S => List[S])
A bit of theory:
Van Laarhoven Lenses
Functors
trait Functor[F[_]]{
def map[A,B](f: A => B)(fa: F[A]): F[B]
}
List Functor
trait Functor[F[_]]{
def map[A,B](f: A => B)(fa: F[A]): F[B]
}
Functor[List]{
def map[A,B](f: A => B)(list: List[A]): List[B] =
list.map(f)
}
Option Functor
trait Functor[F[_]]{
def map[A,B](f: A => B)(fa: F[A]): F[B]
}
Functor[Option]{
def map[A,B](f: A => B)(opt: Option[A]): Option[B] = opt match {
case None => None
case Some(a) => Some(f(a))
}
}
Van Laarhoven Lenses
The answer:
Functor[F] => (A => F[A]) => (S => F[S])
Van Laarhoven Lenses
The answer:
Functor[F] => (A => F[A]) => (S => F[S])
▪
▪
▪ :
▪ modifyMaybe: (A => Option[A]) => (S => Option[S])
▪ modifyList: (A => List[A]) => (S => List[S])
Van Laarhoven Lenses
The answer:
Functor[F] => (A => F[A]) => (S => F[S])
▪ get: S => A
▪ set: A => S => S
▪ modify: (A => A) => (S => S)
▪
▪
Van Laarhoven Lenses
The answer:
lens: Functor[F] => (A => F[A]) => (S => F[S])
type Id[A] = A
lens: (A => Id[A]) => (S => Id[S])
lens: (A => A) => (S => S)
modify = lens[Id]
Identity Functor
trait Functor[F[_]]{
def map[A,B](f: A => B)(fa: F[A]): F[B]
}
type Id[A] = A
Functor[Id]{
def map[A,B] (f: A => B)(a: Id[A]): Id[B]
}
Identity Functor
trait Functor[F[_]]{
def map[A,B](f: A => B)(fa: F[A]): F[B]
}
type Id[A] = A
Functor[Id]{
def map[A,B] (f: A => B)(a: Id[A]): Id[B]
def map[A,B] (f: A => B)(a: A): B = f(a)
}
Van Laarhoven Lenses
The answer:
lens: Functor[F] => (A => F[A]) => (S => F[S])
set(b) = modify(_ => b)
get: S => A = ???
Van Laarhoven Lenses
The answer:
lens: Functor[F] => (A => F[A]) => (S => F[S])
set(b) = modify(_ => b)
get: S => A = ???
Van Laarhoven Lenses
The answer:
lens: Functor[F] => (A => F[A]) => (S => F[S])
type Const[X][T] = X
F = Const[A]
lens: (A => Const[A][A]) => (S => Const[A][S])
lens: (A => A) => (S => A)
get = lens[Const[A]](a => a)
Const Functor
trait Functor[F[_]]{
def map[A,B](f: A => B)(fa: F[A]): F[B]
}
type Const[X][T] = X
Functor[Const[X]]{
def map[A,B] (f: A => B)(fa: Const[X][A]): Const[X][B]
}
Const Functor
trait Functor[F[_]]{
def map[A,B](f: A => B)(fa: F[A]): F[B]
}
type Const[X][T] = X
Functor[Const[X]]{
def map[A,B] (f: A => B)(fa: Const[X][A]): Const[X][B]
def map[A,B] (f: A => B)(x: X): X = x
}
Van Laarhoven Lenses
The answer:
Functor[F] => (A => F[A]) => (S => F[S])
▪ get: S => A
▪ set: A => S => S
▪ modify: (A => A) => (S => S)
▪ modifyMaybe: (A => Option[A]) => (S => Option[S])
▪ modifyList: (A => List[A]) => (S => List[S])
Creating Van Laarhoven Lenses
lens: Functor[F] => (A => F[A]) => (S => F[S])
def get: S => A
def set: A => S => S
def lens[F[_]:Functor](f: A => F[A])(s: S): F[S] =
f(get(s)). map (a => set(a)(s))
A
F[A]
(A => S) =>
F[A] => F[S]
(A => S)
F[S]
Shortly:
lens f s = f(get(s)).map(a => set(a)(s))
Creating Van Laarhoven Lenses
lens: Functor[F] => (A => F[A]) => (S => F[S])
type Id[A] = A
F = Id
lens: (A => A) => (S => S)
lens f s = f(get(s)).map(a => set(a)(s))
[ A ] [ A => S ]
modify f s = set(f(get(s)))(s)
set x s = modify(_ => x) s
= set(x)(s)
Creating Van Laarhoven Lenses
lens: Functor[F] => (A => F[A]) => (S => F[S])
type Const[A][X] = A
F = Const[A]
lens: (A => A) => (S => A)
lens f s = f(get(s)).map(a => set(a)(s))
[ A ] [ A => S ]
get’ f s = f(get(s))
get s = get’(id)(s) = get(s)
Monocle
▪ Provides lots of built-in optics and functions
▪ Macros for creating lenses
▪ Monocle used different models of lenses over time
▪ Current lens representation: PLens – Van Laarhoven hybrid.
▪ Performance: limited by scala function values – pretty good.
Resources
▪ Monocle on github
▪ Simon Peyton Jones’s lens talk at Scala Exchange 2013
▪ Edward Kmett on Lenses with the State Monad
Thank you!
Extra Slides
ASTs - Lenses for APIs
We can simplify our mental model with lenses
case class Person(fullName: String, address: Address)
Case class
decleration
Class
name
List[Field name -> Type]
ASTs - Lenses for APIs
We can simplify our mental model with lenses
case class Person(fullName: String, address: Address)
Case class
decleration
Class
name
List[Field name -> Type]
Public
constructor
Lens[Complex model, Simple model]
val
Polymorphic Optic Instances
How can we use the same lens for many types?
first.set("Hello")((1,2)) == ("Hello",2)
first.set("Hello")((1,2,3)) == ("Hello",2,3)
first.set("Hello")((1,2,3,4)) == ("Hello",2,3,4)
Example: Json
The structure of a specific Json object isn’t defined at the type system;
Each element can be a String or a Number or an Array or an Object.
We want to process Json assuming our specific structure, and let the system handle failures for
us.
We can define Prism[Json, String], Prism[Json, Double], Prism[Json, List[Json]]
and Prism[Json, Map[String, Json]].
Now we can compose these prisms to go deep inside a Json object and manipulate it.
* Polymorphic Lens Composition
get
S
A
T
B
B
set
PLens[S, T, A, B]
get
A
C
B
D
D
set
PLens[A, B, C, D]
get
S
C
T
D
D
set
PLens[S, T, C, D]

Optics with monocle - Modeling the part and the whole

  • 1.
    Optics with Monocle Modelingthe part and the whole By Ilan Godik (@Nightra on IRC)
  • 2.
    About me ▪ ACS Undergraduate at Haifa University ▪ A contributor to the Monocle library ▪ Functional programming lover.
  • 3.
    What are lenses? traitLens[S,A]{ def get(s: S): A def set(a: A)(s: S): S } In the simplest model, it's just a pair of a getter and a setter: get S A1 S A2 set Lens[S, A] A2
  • 4.
    Classic lens example person.copy( address= person.address.copy( street = person.address.street.copy( name = person.address.street.name.capitalize ) ) ) Updating nested structures is verbose and painful case class Person(fullName: String, address: Address) case class Address(city: String, street: Street) case class Street(name: String, number: Int)
  • 5.
    Monocle Lenses (address composeLensstreet composeLens name).modify(_.capitalize)(person) case class Person(fullName: String, address: Address) case class Address(city: String, street: Street) case class Street(name: String, number: Int) val address = Lenser[Person](_.address) val street = Lenser[Address](_.street) val name = Lenser[Street](_.name) Define lenses once, and compose as you wish
  • 6.
    Lenses Macro annotation (addresscomposeLens street composeLens name).modify(_.capitalize)(person) @Lenses case class Person(fullName: String, address: Address) @Lenses case class Address(city: String, street: Street) @Lenses case class Street(name: String, number: Int) import Person._, Address._, Street._ Awesome, but not IDE friendly.
  • 7.
    The object graph Person StringAddress String Street String Int
  • 8.
    Code size growth ▪Vanilla Scala: O(h2) ▪ Lenses: O(h) Where h is the height of the object graph
  • 9.
    Composition: Follow thearrows Person String Address String Street String Int
  • 10.
    Lens composition We composelenses exactly how we would do it with copy. S set Lens[S,A] compose Lens[A,B] A B2 get S A B1 get B2
  • 11.
    Lens composition We composelenses exactly how we would do it with copy. S set Lens[S,A] compose Lens[A,B] A B2 get S A B1 get B2
  • 12.
    Lens composition We composelenses exactly how we would do it with copy. S set Lens[S,A] compose Lens[A,B] A B2 get S A B1 get B2 A B2
  • 13.
    Lens composition We composelenses exactly how we would do it with copy. S set Lens[S,A] compose Lens[A,B] A B2 get S A B1 get B2 A B2
  • 14.
    Polymorphic Lenses Can wechange the type of part of the structure while setting? first.set("Hello")((1,2)) == ("Hello",2)
  • 15.
    Polymorphic Lenses Can wechange the type of part of the structure while setting? trait PLens[S, T, A, B] { def get(s: S): A def set(b: B)(s: S): T } get S A T B B set PLens[S, T, A, B]
  • 16.
    Example: Unit conversion MetersCM MM ? Lens[Meters,CM] Lens[Meters,MM] Optics as different points of view of data
  • 17.
    Isomorphisms Do we getmore power if our lens is bidirectional? Meters CM MM Iso[Meters,CM] Iso[Meters,MM]
  • 18.
    Isomorphisms A B Isomorphisms asa bijection: A function with an inverse
  • 19.
    Prisms What if wedon’t have such a nice correspondence? ? ? A B
  • 20.
    Prisms What if wedon’t have such a nice correspondence? ? ? IntString A B
  • 21.
    Prisms What if wedon’t have such a nice correspondence? ? ? IntString IntString Option[Int]String A B
  • 22.
    Prisms What if wedon’t have such a nice correspondence? ? ? IntString IntString Option[Int]String Laws: 1. If there is an answer, going back must give the source. 2. If we go back, there must be an answer, which is the source. A B
  • 23.
    Property Testing Laws =>Automated property testing Int =String Option[Int] =String _.toString Try(_.toInt).toOption
  • 24.
  • 25.
  • 26.
  • 27.
    Example: Double binding °C30°F86 Iso[Celcius, Fahrenheit] Prism[String, °F]Prism[String, °C]
  • 28.
    Example: Double binding °C30°F86 Iso[Celcius, Fahrenheit] Prism[String, °F]Prism[String, °C]
  • 29.
    Example: Double binding °C30°F86 Iso[Celcius, Fahrenheit] Prism[String, °F]Prism[String, °C]
  • 30.
    A bit oftheory: Van Laarhoven Lenses Is it possible to unify all the lens functions? ▪ get: S => A ▪ set: A => S => S ▪ modify: (A => A) => (S => S)
  • 31.
    Is it possibleto unify all the lens functions? ▪ get: S => A ▪ set: A => S => S ▪ modify: (A => A) => (S => S) ▪ modifyMaybe: (A => Option[A]) => (S => Option[S]) ▪ modifyList: (A => List[A]) => (S => List[S]) A bit of theory: Van Laarhoven Lenses
  • 32.
  • 33.
    List Functor trait Functor[F[_]]{ defmap[A,B](f: A => B)(fa: F[A]): F[B] } Functor[List]{ def map[A,B](f: A => B)(list: List[A]): List[B] = list.map(f) }
  • 34.
    Option Functor trait Functor[F[_]]{ defmap[A,B](f: A => B)(fa: F[A]): F[B] } Functor[Option]{ def map[A,B](f: A => B)(opt: Option[A]): Option[B] = opt match { case None => None case Some(a) => Some(f(a)) } }
  • 35.
    Van Laarhoven Lenses Theanswer: Functor[F] => (A => F[A]) => (S => F[S])
  • 36.
    Van Laarhoven Lenses Theanswer: Functor[F] => (A => F[A]) => (S => F[S]) ▪ ▪ ▪ : ▪ modifyMaybe: (A => Option[A]) => (S => Option[S]) ▪ modifyList: (A => List[A]) => (S => List[S])
  • 37.
    Van Laarhoven Lenses Theanswer: Functor[F] => (A => F[A]) => (S => F[S]) ▪ get: S => A ▪ set: A => S => S ▪ modify: (A => A) => (S => S) ▪ ▪
  • 38.
    Van Laarhoven Lenses Theanswer: lens: Functor[F] => (A => F[A]) => (S => F[S]) type Id[A] = A lens: (A => Id[A]) => (S => Id[S]) lens: (A => A) => (S => S) modify = lens[Id]
  • 39.
    Identity Functor trait Functor[F[_]]{ defmap[A,B](f: A => B)(fa: F[A]): F[B] } type Id[A] = A Functor[Id]{ def map[A,B] (f: A => B)(a: Id[A]): Id[B] }
  • 40.
    Identity Functor trait Functor[F[_]]{ defmap[A,B](f: A => B)(fa: F[A]): F[B] } type Id[A] = A Functor[Id]{ def map[A,B] (f: A => B)(a: Id[A]): Id[B] def map[A,B] (f: A => B)(a: A): B = f(a) }
  • 41.
    Van Laarhoven Lenses Theanswer: lens: Functor[F] => (A => F[A]) => (S => F[S]) set(b) = modify(_ => b) get: S => A = ???
  • 42.
    Van Laarhoven Lenses Theanswer: lens: Functor[F] => (A => F[A]) => (S => F[S]) set(b) = modify(_ => b) get: S => A = ???
  • 43.
    Van Laarhoven Lenses Theanswer: lens: Functor[F] => (A => F[A]) => (S => F[S]) type Const[X][T] = X F = Const[A] lens: (A => Const[A][A]) => (S => Const[A][S]) lens: (A => A) => (S => A) get = lens[Const[A]](a => a)
  • 44.
    Const Functor trait Functor[F[_]]{ defmap[A,B](f: A => B)(fa: F[A]): F[B] } type Const[X][T] = X Functor[Const[X]]{ def map[A,B] (f: A => B)(fa: Const[X][A]): Const[X][B] }
  • 45.
    Const Functor trait Functor[F[_]]{ defmap[A,B](f: A => B)(fa: F[A]): F[B] } type Const[X][T] = X Functor[Const[X]]{ def map[A,B] (f: A => B)(fa: Const[X][A]): Const[X][B] def map[A,B] (f: A => B)(x: X): X = x }
  • 46.
    Van Laarhoven Lenses Theanswer: Functor[F] => (A => F[A]) => (S => F[S]) ▪ get: S => A ▪ set: A => S => S ▪ modify: (A => A) => (S => S) ▪ modifyMaybe: (A => Option[A]) => (S => Option[S]) ▪ modifyList: (A => List[A]) => (S => List[S])
  • 47.
    Creating Van LaarhovenLenses lens: Functor[F] => (A => F[A]) => (S => F[S]) def get: S => A def set: A => S => S def lens[F[_]:Functor](f: A => F[A])(s: S): F[S] = f(get(s)). map (a => set(a)(s)) A F[A] (A => S) => F[A] => F[S] (A => S) F[S] Shortly: lens f s = f(get(s)).map(a => set(a)(s))
  • 48.
    Creating Van LaarhovenLenses lens: Functor[F] => (A => F[A]) => (S => F[S]) type Id[A] = A F = Id lens: (A => A) => (S => S) lens f s = f(get(s)).map(a => set(a)(s)) [ A ] [ A => S ] modify f s = set(f(get(s)))(s) set x s = modify(_ => x) s = set(x)(s)
  • 49.
    Creating Van LaarhovenLenses lens: Functor[F] => (A => F[A]) => (S => F[S]) type Const[A][X] = A F = Const[A] lens: (A => A) => (S => A) lens f s = f(get(s)).map(a => set(a)(s)) [ A ] [ A => S ] get’ f s = f(get(s)) get s = get’(id)(s) = get(s)
  • 50.
    Monocle ▪ Provides lotsof built-in optics and functions ▪ Macros for creating lenses ▪ Monocle used different models of lenses over time ▪ Current lens representation: PLens – Van Laarhoven hybrid. ▪ Performance: limited by scala function values – pretty good.
  • 51.
    Resources ▪ Monocle ongithub ▪ Simon Peyton Jones’s lens talk at Scala Exchange 2013 ▪ Edward Kmett on Lenses with the State Monad
  • 52.
  • 53.
  • 54.
    ASTs - Lensesfor APIs We can simplify our mental model with lenses case class Person(fullName: String, address: Address) Case class decleration Class name List[Field name -> Type]
  • 55.
    ASTs - Lensesfor APIs We can simplify our mental model with lenses case class Person(fullName: String, address: Address) Case class decleration Class name List[Field name -> Type] Public constructor Lens[Complex model, Simple model] val
  • 56.
    Polymorphic Optic Instances Howcan we use the same lens for many types? first.set("Hello")((1,2)) == ("Hello",2) first.set("Hello")((1,2,3)) == ("Hello",2,3) first.set("Hello")((1,2,3,4)) == ("Hello",2,3,4)
  • 57.
    Example: Json The structureof a specific Json object isn’t defined at the type system; Each element can be a String or a Number or an Array or an Object. We want to process Json assuming our specific structure, and let the system handle failures for us. We can define Prism[Json, String], Prism[Json, Double], Prism[Json, List[Json]] and Prism[Json, Map[String, Json]]. Now we can compose these prisms to go deep inside a Json object and manipulate it.
  • 58.
    * Polymorphic LensComposition get S A T B B set PLens[S, T, A, B] get A C B D D set PLens[A, B, C, D] get S C T D D set PLens[S, T, C, D]