+ - 0:00:00
Notes for current slide
Notes for next slide

Optics from the ground up

Nicolas Rinaudo • @NicolasRinaudoBesedo

1 / 266

Algebraic Data Types

2 / 266

Product types

case class Classifier(
name : String,
classCount: Int
)
3 / 266

Product types

case class Classifier(
name : String,
classCount: Int
)
4 / 266

Product types

case class Classifier(
name : String,
classCount: Int
)
5 / 266

Product types

case class Classifier(
name : String,
classCount: Int
)
6 / 266

Product types

Classifier

7 / 266

Product types

Classifier

8 / 266

Product types

Classifier

9 / 266

Product types

Classifier

10 / 266

Product types

Classifier

11 / 266

Sum types

sealed trait Auth
case class Token(
token: String
) extends Auth
case class Login(
user : String,
password: String
) extends Auth
12 / 266

Sum types

sealed trait Auth
case class Token(
token: String
) extends Auth
case class Login(
user : String,
password: String
) extends Auth
13 / 266

Sum types

sealed trait Auth
case class Token(
token: String
) extends Auth
case class Login(
user : String,
password: String
) extends Auth
14 / 266

Sum types

sealed trait Auth
case class Token(
token: String
) extends Auth
case class Login(
user : String,
password: String
) extends Auth
15 / 266

Sum types

Auth

16 / 266

Sum types

Auth

17 / 266

Sum types

Auth

18 / 266

Sum types

Auth

19 / 266

Sum types

Auth

20 / 266

ADTs

case class MlService(
auth : Auth,
classifier: Classifier
)
21 / 266

ADTs

case class MlService(
auth : Auth,
classifier: Classifier
)
22 / 266

ADTs

case class MlService(
auth : Auth,
classifier: Classifier
)
23 / 266

ADTs

case class MlService(
auth : Auth,
classifier: Classifier
)
24 / 266

ADTs

MlService

25 / 266

ADTs

MlService

26 / 266

ADTs

MlService

27 / 266

ADTs

MlService

28 / 266

ADTs

MlService

29 / 266

Key takeaways

30 / 266

Key takeaways

  • Product types aggregate values with an .
31 / 266

Key takeaways

  • Product types aggregate values with an .
  • Sum types aggregate values with an .
32 / 266

Key takeaways

  • Product types aggregate values with an .
  • Sum types aggregate values with an .
  • ADTs are nested sum and product types.
33 / 266

Lenses

34 / 266

Motivation

val service = MlService(
Login("jsmith", "Tr0ub4dor&3"),
Classifier("news20", 20)
)
35 / 266

Motivation

val service = MlService(
Login("jsmith", "Tr0ub4dor&3"),
Classifier("news20", 20)
)
36 / 266

Motivation

Complete ADT

37 / 266

Motivation

Service to classifier name

38 / 266

Motivation

Java

service.classifier.name = "NEWS20"
39 / 266

Motivation

Java

service.classifier.name = "NEWS20"

Scala

service.copy(
classifier = service.classifier.copy(
name = "NEWS20"
)
)
40 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
}
val nameSetter: Setter[MlService, String] = ???
val updated: MlService =
nameSetter.set("NEWS20")(service)
41 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
}
val nameSetter: Setter[MlService, String] = ???
val updated: MlService =
nameSetter.set("NEWS20")(service)
42 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
}
val nameSetter: Setter[MlService, String] = ???
val updated: MlService =
nameSetter.set("NEWS20")(service)
43 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
}
val nameSetter: Setter[MlService, String] = ???
val updated: MlService =
nameSetter.set("NEWS20")(service)
44 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
}
val nameSetter: Setter[MlService, String] = ???
val updated: MlService =
nameSetter.set("NEWS20")(service)
45 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
}
val nameSetter: Setter[MlService, String] = ???
val updated: MlService =
nameSetter.set("NEWS20")(service)
46 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
}
val nameSetter: Setter[MlService, String] = ???
val updated: MlService =
nameSetter.set("NEWS20")(service)
47 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
def get(s: S): A
}
val classifierName: String =
nameSetter.get(service)
48 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
def get(s: S): A
}
val classifierName: String =
nameSetter.get(service)
49 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
def get(s: S): A
}
val classifierName: String =
nameSetter.get(service)
50 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
def get(s: S): A
}
val classifierName: String =
nameSetter.get(service)
51 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
def get(s: S): A
def modify(f: A => A)(s: S): S = set(f(get(s)))(s)
}
val updated: MlService =
nameSetter.modify(_.toUpperCase)(service)
52 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
def get(s: S): A
def modify(f: A => A)(s: S): S = set(f(get(s)))(s)
}
val updated: MlService =
nameSetter.modify(_.toUpperCase)(service)
53 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
def get(s: S): A
def modify(f: A => A)(s: S): S = set(f(get(s)))(s)
}
val updated: MlService =
nameSetter.modify(_.toUpperCase)(service)
54 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
def get(s: S): A
def modify(f: A => A)(s: S): S = set(f(get(s)))(s)
}
val updated: MlService =
nameSetter.modify(_.toUpperCase)(service)
55 / 266

Generic setter type

trait Setter[S, A] {
def set(a: A)(s: S): S
def get(s: S): A
def modify(f: A => A)(s: S): S = set(f(get(s)))(s)
}
val updated: MlService =
nameSetter.modify(_.toUpperCase)(service)
56 / 266

Lens

trait Lens[S, A] {
def set(a: A)(s: S): S
def get(s: S): A
def modify(f: A => A)(s: S): S = set(f(get(s)))(s)
}
57 / 266

Lens

trait Lens[S, A] {
def set(a: A)(s: S): S
def get(s: S): A
def modify(f: A => A)(s: S): S = set(f(get(s)))(s)
}
58 / 266

Lens

object Lens {
def apply[S, A](
setter: (A, S) => S,
getter: S => A
) = new Lens[S, A] {
override def set(a: A)(s: S) = setter(a, s)
override def get(s: S) = getter(s)
}
}
59 / 266

Lens

object Lens {
def apply[S, A](
setter: (A, S) => S,
getter: S => A
) = new Lens[S, A] {
override def set(a: A)(s: S) = setter(a, s)
override def get(s: S) = getter(s)
}
}
60 / 266

Lens

object Lens {
def apply[S, A](
setter: (A, S) => S,
getter: S => A
) = new Lens[S, A] {
override def set(a: A)(s: S) = setter(a, s)
override def get(s: S) = getter(s)
}
}
61 / 266

Lens

object Lens {
def apply[S, A](
setter: (A, S) => S,
getter: S => A
) = new Lens[S, A] {
override def set(a: A)(s: S) = setter(a, s)
override def get(s: S) = getter(s)
}
}
62 / 266

Lens

object Lens {
def apply[S, A](
setter: (A, S) => S,
getter: S => A
) = new Lens[S, A] {
override def set(a: A)(s: S) = setter(a, s)
override def get(s: S) = getter(s)
}
}
63 / 266

Lens

type Set[S, A] = (A, S) => S
type Get[S, A] = S => A
64 / 266

Lens

type Set[S, A] = (A, S) => S
type Get[S, A] = S => A
65 / 266

Lens

type Set[S, A] = (A, S) => S
type Get[S, A] = S => A
66 / 266

Lens

object Lens {
def apply[S, A](
setter: Set[S, A],
getter: Get[S, A]
) = new Lens[S, A] {
override def set(a: A)(s: S) = setter(a, s)
override def get(s: S) = getter(s)
}
}
67 / 266

Lens

object Lens {
def apply[S, A](
setter: Set[S, A],
getter: Get[S, A]
) = new Lens[S, A] {
override def set(a: A)(s: S) = setter(a, s)
override def get(s: S) = getter(s)
}
}
68 / 266

Service → Classifier name

Service to classifier name

69 / 266

Service → Classifier

Service to classifier

70 / 266

Service → Classifier

val serviceClassifier = Lens[MlService, Classifier](
setter = (a, s) => s.copy(classifier = a),
getter = s => s.classifier
)
71 / 266

Service → Classifier

val serviceClassifier = Lens[MlService, Classifier](
setter = (a, s) => s.copy(classifier = a),
getter = s => s.classifier
)
72 / 266

Service → Classifier

val serviceClassifier = Lens[MlService, Classifier](
setter = (a, s) => s.copy(classifier = a),
getter = s => s.classifier
)
73 / 266

Service → Classifier

val serviceClassifier = Lens[MlService, Classifier](
setter = (a, s) => s.copy(classifier = a),
getter = s => s.classifier
)
74 / 266

Classifier → name

Classifier to name

75 / 266

Classifier → name

val classifierName = Lens[Classifier, String](
setter = (a, s) => s.copy(name = a),
getter = s => s.name
)
76 / 266

Classifier → name

serviceClassifier.modify(classifierName.set("NEWS20"))(service)
// res1: MlService = MlService(Login(jsmith,Tr0ub4dor&3),Classifier(NEWS20,20))
77 / 266

Composing lenses

def setName(name: String, service: MlService) =
serviceClassifier.modify(classifierName.set(name))(service)
78 / 266

Composing lenses

def setName(name: String, service: MlService) =
serviceClassifier.modify(classifierName.set(name))(service)
79 / 266

Composing lenses

def setName(name: String, service: MlService) =
serviceClassifier.modify(classifierName.set(name))(service)
80 / 266

Composing lenses

def setName(
serviceClassifier: Lens[MlService, Classifier],
classifierName : Lens[Classifier, String]
)
(name: String, service: MlService): MlService =
serviceClassifier.modify(classifierName.set(name))(service)
81 / 266

Composing lenses

def setName(
serviceClassifier: Lens[MlService, Classifier],
classifierName : Lens[Classifier, String]
)
(name: String, service: MlService): MlService =
serviceClassifier.modify(classifierName.set(name))(service)
82 / 266

Composing lenses

def setName(
serviceClassifier: Lens[MlService, Classifier],
classifierName : Lens[Classifier, String]
)
(name: String, service: MlService): MlService =
serviceClassifier.modify(classifierName.set(name))(service)
83 / 266

Composing lenses

def setName(
serviceClassifier: Lens[MlService, Classifier],
classifierName : Lens[Classifier, String]
)
(name: String, service: MlService): MlService =
serviceClassifier.modify(classifierName.set(name))(service)
84 / 266

Composing lenses

def setter[S, A, B](
l1: Lens[S, A],
l2: Lens[A, B]
)
(b: B, s: S): S =
l1.modify(l2.set(b))(s)
85 / 266

Composing lenses

def setter[S, A, B](
l1: Lens[S, A],
l2: Lens[A, B]
)
(b: B, s: S): S =
l1.modify(l2.set(b))(s)
86 / 266

Composing lenses

def setter[S, A, B](
l1: Lens[S, A],
l2: Lens[A, B]
)
(b: B, s: S): S =
l1.modify(l2.set(b))(s)
87 / 266

Composing lenses

def setter[S, A, B](
l1: Lens[S, A],
l2: Lens[A, B]
)
: Set[S, B] = (b, s) =>
l1.modify(l2.set(b))(s)
88 / 266

Composing lenses

def setter[S, A, B](
l1: Lens[S, A],
l2: Lens[A, B]
)
: Set[S, B] = (b, s) =>
l1.modify(l2.set(b))(s)
89 / 266

Composing lenses

def getter[S, A, B](
l1: Lens[S, A],
l2: Lens[A, B]
)
: Get[S, B] = s =>
l2.get(l1.get(s))
90 / 266

Composing lenses

def getter[S, A, B](
l1: Lens[S, A],
l2: Lens[A, B]
)
: Get[S, B] = s =>
l2.get(l1.get(s))
91 / 266

Composing lenses

def getter[S, A, B](
l1: Lens[S, A],
l2: Lens[A, B]
)
: Get[S, B] = s =>
l2.get(l1.get(s))
92 / 266

Composing lenses

def getter[S, A, B](
l1: Lens[S, A],
l2: Lens[A, B]
)
: Get[S, B] = s =>
l2.get(l1.get(s))
93 / 266

Composing lenses

def composeLL[S, A, B](
l1: Lens[S, A],
l2: Lens[A, B]
) = Lens[S, B](
setter(l1, l2),
getter(l1, l2)
)
94 / 266

Composing lenses

def composeLL[S, A, B](
l1: Lens[S, A],
l2: Lens[A, B]
) = Lens[S, B](
setter(l1, l2),
getter(l1, l2)
)
95 / 266

Composing lenses

def composeLL[S, A, B](
l1: Lens[S, A],
l2: Lens[A, B]
) = Lens[S, B](
setter(l1, l2),
getter(l1, l2)
)
96 / 266

Composing lenses

def composeLL[S, A, B](
l1: Lens[S, A],
l2: Lens[A, B]
) = Lens[S, B](
setter(l1, l2),
getter(l1, l2)
)
97 / 266

Service → Classifier → name

Service to classifier name

98 / 266

Service → Classifier → name

val serviceClassifierName = composeLL(
serviceClassifier,
classifierName
)
99 / 266

Service → Classifier → name

serviceClassifierName.modify(_.toUpperCase)(service)
// res2: MlService = MlService(Login(jsmith,Tr0ub4dor&3),Classifier(NEWS20,20))
100 / 266

Key takeaways

101 / 266

Key takeaways

  • Lenses are used to focus on specific parts of a product type.
102 / 266

Key takeaways

  • Lenses are used to focus on specific parts of a product type.
  • They compose to build arbitrarily deep paths in nested structures.
103 / 266

Prisms & Optionals

104 / 266

Motivation

val service = MlService(
Login("jsmith", "Tr0ub4dor&3"),
Classifier("news20", 20)
)
105 / 266

Motivation

val service = MlService(
Login("jsmith", "Tr0ub4dor&3"),
Classifier("news20", 20)
)
106 / 266

Motivation

Complete ADT

107 / 266

Motivation

Service to user

108 / 266

Service → Auth

Service to auth

109 / 266

Service → Auth

val serviceAuth = Lens[MlService, Auth](
setter = (a, s) => s.copy(auth = a),
getter = s => s.auth
)
110 / 266

Auth → Login

Auth to login

111 / 266

Auth → user?

Auth to user

112 / 266

Auth → user?

val authUser = Lens[Auth, String](
setter = (a, s) => ???,
// Auth -> String
getter = s => s match {
case Login(user, _) => user
case Token(token) => ???
}
)
113 / 266

Auth → user?

val authUser = Lens[Auth, String](
setter = (a, s) => ???,
// Auth -> String
getter = s => s match {
case Login(user, _) => user
case Token(token) => ???
}
)
114 / 266

Auth → user?

val authUser = Lens[Auth, String](
setter = (a, s) => ???,
// Auth -> String
getter = s => s match {
case Login(user, _) => user
case Token(token) => ???
}
)
115 / 266

Auth → user?

val authUser = Lens[Auth, String](
setter = (a, s) => ???,
// Auth -> String
getter = s => s match {
case Login(user, _) => user
case Token(token) => ???
}
)
116 / 266

Auth → user?

val authUser = Lens[Auth, String](
setter = (a, s) => ???,
// Auth -> String
getter = s => s match {
case Login(user, _) => user
case Token(token) => ???
}
)
117 / 266

Auth → user?

val authUser = Lens[Auth, String](
setter = (a, s) => ???,
// Auth -> String
getter = s => s match {
case Login(user, _) => user
case Token(token) => ???
}
)
118 / 266

Auth → Login

Auth to login

119 / 266

Prism

trait Prism[S, A] {
def set(a: A): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))
case None => s
}
}
val authLogin: Prism[Auth, Login] = ???
val updated: Auth = authLogin.set(Login("foo", "bar"))
val login: Option[Login] = authLogin.get(service.auth)
120 / 266

Prism

trait Prism[S, A] {
def set(a: A): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))
case None => s
}
}
val authLogin: Prism[Auth, Login] = ???
val updated: Auth = authLogin.set(Login("foo", "bar"))
val login: Option[Login] = authLogin.get(service.auth)
121 / 266

Prism

trait Prism[S, A] {
def set(a: A): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))
case None => s
}
}
val authLogin: Prism[Auth, Login] = ???
val updated: Auth = authLogin.set(Login("foo", "bar"))
val login: Option[Login] = authLogin.get(service.auth)
122 / 266

Prism

trait Prism[S, A] {
def set(a: A): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))
case None => s
}
}
val authLogin: Prism[Auth, Login] = ???
val updated: Auth = authLogin.set(Login("foo", "bar"))
val login: Option[Login] = authLogin.get(service.auth)
123 / 266

Prism

trait Prism[S, A] {
def set(a: A): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))
case None => s
}
}
val authLogin: Prism[Auth, Login] = ???
val updated: Auth = authLogin.set(Login("foo", "bar"))
val login: Option[Login] = authLogin.get(service.auth)
124 / 266

Prism

trait Prism[S, A] {
def set(a: A): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))
case None => s
}
}
val authLogin: Prism[Auth, Login] = ???
val updated: Auth = authLogin.set(Login("foo", "bar"))
val login: Option[Login] = authLogin.get(service.auth)
125 / 266

Prism

trait Prism[S, A] {
def set(a: A): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))
case None => s
}
}
val authLogin: Prism[Auth, Login] = ???
val updated: Auth = authLogin.set(Login("foo", "bar"))
val login: Option[Login] = authLogin.get(service.auth)
126 / 266

Prism

trait Prism[S, A] {
def set(a: A): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))
case None => s
}
}
val authLogin: Prism[Auth, Login] = ???
val updated: Auth = authLogin.set(Login("foo", "bar"))
val login: Option[Login] = authLogin.get(service.auth)
127 / 266

Prism

trait Prism[S, A] {
def set(a: A): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))
case None => s
}
}
val authLogin: Prism[Auth, Login] = ???
val updated: Auth = authLogin.set(Login("foo", "bar"))
val login: Option[Login] = authLogin.get(service.auth)
128 / 266

Prism

object Prism {
def apply[S, A](
setter: A => S,
getter: S => Option[A]
) = new Prism[S, A] {
override def set(a: A) = setter(a)
override def get(s: S) = getter(s)
}
}
129 / 266

Prism

object Prism {
def apply[S, A](
setter: A => S,
getter: S => Option[A]
) = new Prism[S, A] {
override def set(a: A) = setter(a)
override def get(s: S) = getter(s)
}
}
130 / 266

Prism

object Prism {
def apply[S, A](
setter: A => S,
getter: S => Option[A]
) = new Prism[S, A] {
override def set(a: A) = setter(a)
override def get(s: S) = getter(s)
}
}
131 / 266

Prism

object Prism {
def apply[S, A](
setter: A => S,
getter: S => Option[A]
) = new Prism[S, A] {
override def set(a: A) = setter(a)
override def get(s: S) = getter(s)
}
}
132 / 266

Prism

object Prism {
def apply[S, A](
setter: A => S,
getter: S => Option[A]
) = new Prism[S, A] {
override def set(a: A) = setter(a)
override def get(s: S) = getter(s)
}
}
133 / 266

Prism

object Prism {
def apply[S, A](
setter: A => S,
getter: S => Option[A]
) = new Prism[S, A] {
override def set(a: A) = setter(a)
override def get(s: S) = getter(s)
}
}
134 / 266

Prism

object Prism {
def apply[S, A](
setter: A => S,
getter: S => Option[A]
) = new Prism[S, A] {
override def set(a: A) = setter(a)
override def get(s: S) = getter(s)
}
def fromPartial[S, A](
setter: A => S,
getter: PartialFunction[S, A]
) = Prism(setter, getter.lift)
}
135 / 266

Prism

object Prism {
def apply[S, A](
setter: A => S,
getter: S => Option[A]
) = new Prism[S, A] {
override def set(a: A) = setter(a)
override def get(s: S) = getter(s)
}
def fromPartial[S, A](
setter: A => S,
getter: PartialFunction[S, A]
) = Prism(setter, getter.lift)
}
136 / 266

Prism

object Prism {
def apply[S, A](
setter: A => S,
getter: S => Option[A]
) = new Prism[S, A] {
override def set(a: A) = setter(a)
override def get(s: S) = getter(s)
}
def fromPartial[S, A](
setter: A => S,
getter: PartialFunction[S, A]
) = Prism(setter, getter.lift)
}
137 / 266

Prism

object Prism {
def apply[S, A](
setter: A => S,
getter: S => Option[A]
) = new Prism[S, A] {
override def set(a: A) = setter(a)
override def get(s: S) = getter(s)
}
def fromPartial[S, A](
setter: A => S,
getter: PartialFunction[S, A]
) = Prism(setter, getter.lift)
}
138 / 266

Auth → Login

Auth to login

139 / 266

Auth → Login

val authLogin = Prism.fromPartial[Auth, Login](
setter = a => a,
getter = { case s: Login => s }
)
140 / 266

Auth → Login

val authLogin = Prism.fromPartial[Auth, Login](
setter = a => a,
getter = { case s: Login => s }
)
141 / 266

Auth → Login

val authLogin = Prism.fromPartial[Auth, Login](
setter = a => a,
getter = { case s: Login => s }
)
142 / 266

Auth → Login

val authLogin = Prism.fromPartial[Auth, Login](
setter = a => a,
getter = { case s: Login => s }
)
143 / 266

Service → Login

Service to login

144 / 266

Lens ∘ Prism ≟ Prism

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Prism[S, B](
// B => S
setter = b => {
val a: A = p.set(b)
val s: S = ???
s
},
// S => B
getter = s => p.get(l.get(s))
)
145 / 266

Lens ∘ Prism ≟ Prism

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Prism[S, B](
// B => S
setter = b => {
val a: A = p.set(b)
val s: S = ???
s
},
// S => B
getter = s => p.get(l.get(s))
)
146 / 266

Lens ∘ Prism ≟ Prism

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Prism[S, B](
// B => S
setter = b => {
val a: A = p.set(b)
val s: S = ???
s
},
// S => B
getter = s => p.get(l.get(s))
)
147 / 266

Lens ∘ Prism ≟ Prism

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Prism[S, B](
// B => S
setter = b => {
val a: A = p.set(b)
val s: S = ???
s
},
// S => B
getter = s => p.get(l.get(s))
)
148 / 266

Lens ∘ Prism ≟ Prism

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Prism[S, B](
// B => S
setter = b => {
val a: A = p.set(b)
val s: S = ???
s
},
// S => B
getter = s => p.get(l.get(s))
)
149 / 266

Lens ∘ Prism ≟ Prism

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Prism[S, B](
// B => S
setter = b => {
val a: A = p.set(b)
val s: S = ???
s
},
// S => B
getter = s => p.get(l.get(s))
)
150 / 266

Lens ∘ Prism ≟ Prism

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Prism[S, B](
// B => S
setter = b => {
val a: A = p.set(b)
val s: S = ???
s
},
// S => B
getter = s => p.get(l.get(s))
)
151 / 266

Lens ∘ Prism ≟ Lens

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Lens[S, B](
// (B, S) => S
setter = (b, s) => l.set(p.set(b))(s),
// S => B
getter = s => {
val a: A = l.get(s)
val b: B = ???
b
}
)
152 / 266

Lens ∘ Prism ≟ Lens

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Lens[S, B](
// (B, S) => S
setter = (b, s) => l.set(p.set(b))(s),
// S => B
getter = s => {
val a: A = l.get(s)
val b: B = ???
b
}
)
153 / 266

Lens ∘ Prism ≟ Lens

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Lens[S, B](
// (B, S) => S
setter = (b, s) => l.set(p.set(b))(s),
// S => B
getter = s => {
val a: A = l.get(s)
val b: B = ???
b
}
)
154 / 266

Lens ∘ Prism ≟ Lens

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Lens[S, B](
// (B, S) => S
setter = (b, s) => l.set(p.set(b))(s),
// S => B
getter = s => {
val a: A = l.get(s)
val b: B = ???
b
}
)
155 / 266

Lens ∘ Prism ≟ Lens

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Lens[S, B](
// (B, S) => S
setter = (b, s) => l.set(p.set(b))(s),
// S => B
getter = s => {
val a: A = l.get(s)
val b: B = ???
b
}
)
156 / 266

Lens ∘ Prism ≟ Lens

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Lens[S, B](
// (B, S) => S
setter = (b, s) => l.set(p.set(b))(s),
// S => B
getter = s => {
val a: A = l.get(s)
val b: B = ???
b
}
)
157 / 266

Lens ∘ Prism ≟ Lens

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Lens[S, B](
// (B, S) => S
setter = (b, s) => l.set(p.set(b))(s),
// S => B
getter = s => {
val a: A = l.get(s)
val b: B = ???
b
}
)
158 / 266

Optional

trait Optional[S, A] {
def set(a: A)(s: S): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))(s)
case None => s
}
}
val serviceLogin: Optional[MlService, Login] = ???
val updated: MlService =
serviceLogin.set(Login("psmith", "Tr0ub4dor&3"))(service)
val login: Option[Login] = serviceLogin.get(service)
159 / 266

Optional

trait Optional[S, A] {
def set(a: A)(s: S): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))(s)
case None => s
}
}
val serviceLogin: Optional[MlService, Login] = ???
val updated: MlService =
serviceLogin.set(Login("psmith", "Tr0ub4dor&3"))(service)
val login: Option[Login] = serviceLogin.get(service)
160 / 266

Optional

trait Optional[S, A] {
def set(a: A)(s: S): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))(s)
case None => s
}
}
val serviceLogin: Optional[MlService, Login] = ???
val updated: MlService =
serviceLogin.set(Login("psmith", "Tr0ub4dor&3"))(service)
val login: Option[Login] = serviceLogin.get(service)
161 / 266

Optional

trait Optional[S, A] {
def set(a: A)(s: S): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))(s)
case None => s
}
}
val serviceLogin: Optional[MlService, Login] = ???
val updated: MlService =
serviceLogin.set(Login("psmith", "Tr0ub4dor&3"))(service)
val login: Option[Login] = serviceLogin.get(service)
162 / 266

Optional

trait Optional[S, A] {
def set(a: A)(s: S): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))(s)
case None => s
}
}
val serviceLogin: Optional[MlService, Login] = ???
val updated: MlService =
serviceLogin.set(Login("psmith", "Tr0ub4dor&3"))(service)
val login: Option[Login] = serviceLogin.get(service)
163 / 266

Optional

trait Optional[S, A] {
def set(a: A)(s: S): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))(s)
case None => s
}
}
val serviceLogin: Optional[MlService, Login] = ???
val updated: MlService =
serviceLogin.set(Login("psmith", "Tr0ub4dor&3"))(service)
val login: Option[Login] = serviceLogin.get(service)
164 / 266

Optional

trait Optional[S, A] {
def set(a: A)(s: S): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))(s)
case None => s
}
}
val serviceLogin: Optional[MlService, Login] = ???
val updated: MlService =
serviceLogin.set(Login("psmith", "Tr0ub4dor&3"))(service)
val login: Option[Login] = serviceLogin.get(service)
165 / 266

Optional

trait Optional[S, A] {
def set(a: A)(s: S): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))(s)
case None => s
}
}
val serviceLogin: Optional[MlService, Login] = ???
val updated: MlService =
serviceLogin.set(Login("psmith", "Tr0ub4dor&3"))(service)
val login: Option[Login] = serviceLogin.get(service)
166 / 266

Optional

trait Optional[S, A] {
def set(a: A)(s: S): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))(s)
case None => s
}
}
val serviceLogin: Optional[MlService, Login] = ???
val updated: MlService =
serviceLogin.set(Login("psmith", "Tr0ub4dor&3"))(service)
val login: Option[Login] = serviceLogin.get(service)
167 / 266

Optional

trait Optional[S, A] {
def set(a: A)(s: S): S
def get(s: S): Option[A]
def modify(f: A => A)(s: S): S = get(s) match {
case Some(a) => set(f(a))(s)
case None => s
}
}
val serviceLogin: Optional[MlService, Login] = ???
val updated: MlService =
serviceLogin.set(Login("psmith", "Tr0ub4dor&3"))(service)
val login: Option[Login] = serviceLogin.get(service)
168 / 266

Optional

object Optional {
def apply[S, A](
setter: (A, S) => S,
getter: S => Option[A]
) = new Optional[S, A] {
override def set(a: A)(s: S) = setter(a, s)
override def get(s: S) = getter(s)
}
}
169 / 266

Lens ∘ Prism = Optional

def composeLP[S, A, B](
l: Lens[S, A],
p: Prism[A, B]
) = Optional[S, B](
setter = (b, s) => l.set(p.set(b))(s),
getter = s => p.get(l.get(s))
)
170 / 266

Intermission: composition galore

171 / 266

Prism ∘ Prism = Prism

def composePP[S, A, B](
p1: Prism[S, A],
p2: Prism[A, B]
) = Prism[S, B](
setter = b => p1.set(p2.set(b)),
getter = s => p1.get(s).flatMap(p2.get)
)
172 / 266

Prism ∘ Lens = Optional

def composePL[S, A, B](
p: Prism[S, A],
l: Lens[A, B]
) = Optional[S, B](
setter = (b, s) => p.modify(l.set(b))(s),
getter = s => p.get(s).map(l.get)
)
173 / 266

Optional ∘ Optional = Optional

def composeOO[S, A, B](
o1: Optional[S, A],
o2: Optional[A, B]
) = Optional[S, B](
setter = (b, s) => o1.modify(o2.set(b))(s),
getter = s => o1.get(s).flatMap(o2.get)
)
174 / 266

Optional ∘ Prism = Optional

def composeOP[S, A, B](
o: Optional[S, A],
p: Prism[A, B]
) = Optional[S, B](
setter = (b, s) => o.set(p.set(b))(s),
getter = s => o.get(s).flatMap(p.get)
)
175 / 266

Prism ∘ Optional = Optional

def composePO[S, A, B](
p: Prism[S, A],
o: Optional[A, B]
) = Optional[S, B](
setter = (b, s) => p.modify(o.set(b))(s),
getter = s => p.get(s).flatMap(o.get)
)
176 / 266

Optional ∘ Lens = Optional

def composeOL[S, A, B](
o: Optional[S, A],
l: Lens[A, B]
) = Optional[S, B](
setter = (b, s) => o.modify(l.set(b))(s),
getter = s => o.get(s).map(l.get)
)
177 / 266

Lens ∘ Optional = Optional

def composeLO[S, A, B](
l: Lens[S, A],
o: Optional[A, B]
) = Optional[S, B](
setter = (b, s) => l.modify(o.set(b))(s),
getter = s => o.get(l.get(s))
)
178 / 266

End of intermission

179 / 266

Service → Login

Service to login

180 / 266

Service → Login

val serviceLogin = composeLP(
serviceAuth,
authLogin
)
181 / 266

Login → user

Login to user

182 / 266

Login → user

val loginUser = Lens[Login, String](
setter = (a, s) => s.copy(user = a),
getter = s => s.user
)
183 / 266

Service → user

Service to user

184 / 266

Service → user

val serviceUser = composeOL(
serviceLogin,
loginUser
)
185 / 266

Service → user

serviceUser.set("psmith")(service)
// res3: MlService = MlService(Login(psmith,Tr0ub4dor&3),Classifier(news20,20))
186 / 266

Key takeaways

187 / 266

Key takeaways

  • Prisms are used to explore sum types.
188 / 266

Key takeaways

  • Prisms are used to explore sum types.
  • They compose with lenses and yield optionals.
189 / 266

Concrete use case: ConfigPath

190 / 266

Configuration data structure

{
"auth": {
"user" : "psmith",
"password": "Tr0ub4dor&3"
},
"classifier": {
"name" : "news20",
"classCount": 20
}
}
191 / 266

Configuration data structure

{
"auth": {
"user" : "psmith",
"password": "Tr0ub4dor&3"
},
"classifier": {
"name" : "news20",
"classCount": 20
}
}
192 / 266

Configuration data structure

{
"auth": {
"user" : "psmith",
"password": "Tr0ub4dor&3"
},
"classifier": {
"name" : "news20",
"classCount": 20
}
}
193 / 266

Configuration data structure

{
"auth": {
"user" : "psmith",
"password": "Tr0ub4dor&3"
},
"classifier": {
"name" : "news20",
"classCount": 20
}
}
194 / 266

Configuration data structure

sealed trait Config
case class Value(
value: String
) extends Config
case class Section(
children: Map[String, Config]
) extends Config
195 / 266

Configuration data structure

sealed trait Config
case class Value(
value: String
) extends Config
case class Section(
children: Map[String, Config]
) extends Config
196 / 266

Configuration data structure

sealed trait Config
case class Value(
value: String
) extends Config
case class Section(
children: Map[String, Config]
) extends Config
197 / 266

Configuration data structure

sealed trait Config
case class Value(
value: String
) extends Config
case class Section(
children: Map[String, Config]
) extends Config
198 / 266

Configuration data structure

sealed trait Config
case class Value(
value: String
) extends Config
case class Section(
children: Map[String, Config]
) extends Config
199 / 266

Configuration data structure

sealed trait Config
case class Value(
value: String
) extends Config
case class Section(
children: Map[String, Config]
) extends Config
200 / 266

Configuration data structure

sealed trait Config
case class Value(
value: String
) extends Config
case class Section(
children: Map[String, Config]
) extends Config
201 / 266

Configuration data structure

sealed trait Config
case class Value(
value: String
) extends Config
case class Section(
children: Map[String, Config]
) extends Config
202 / 266

Configuration data structure

val conf = Section(Map(
"auth" -> Section(Map(
"user" -> Value("psmith"),
"password" -> Value("Tr0ub4dor&3")
)),
"classifier" -> Section(Map(
"name" -> Value("news20"),
"classCount" -> Value("20")
))
))
203 / 266

Configuration data structure

val conf = Section(Map(
"auth" -> Section(Map(
"user" -> Value("psmith"),
"password" -> Value("Tr0ub4dor&3")
)),
"classifier" -> Section(Map(
"name" -> Value("news20"),
"classCount" -> Value("20")
))
))
204 / 266

Configuration data structure

val conf = Section(Map(
"auth" -> Section(Map(
"user" -> Value("psmith"),
"password" -> Value("Tr0ub4dor&3")
)),
"classifier" -> Section(Map(
"name" -> Value("news20"),
"classCount" -> Value("20")
))
))
205 / 266

Configuration data structure

val conf = Section(Map(
"auth" -> Section(Map(
"user" -> Value("psmith"),
"password" -> Value("Tr0ub4dor&3")
)),
"classifier" -> Section(Map(
"name" -> Value("news20"),
"classCount" -> Value("20")
))
))
206 / 266

Configuration optics

val section = Prism.fromPartial[Config, Section](
setter = a => a,
getter = { case a: Section => a }
)
val value = Prism.fromPartial[Config, Value](
setter = a => a,
getter = { case a: Value => a }
)
207 / 266

Configuration optics

val section = Prism.fromPartial[Config, Section](
setter = a => a,
getter = { case a: Section => a }
)
val value = Prism.fromPartial[Config, Value](
setter = a => a,
getter = { case a: Value => a }
)
208 / 266

Configuration optics

val section = Prism.fromPartial[Config, Section](
setter = a => a,
getter = { case a: Section => a }
)
val value = Prism.fromPartial[Config, Value](
setter = a => a,
getter = { case a: Value => a }
)
209 / 266

Configuration optics

def sectionChild(name: String) = Optional[Section, Config](
setter = (a, s) => Section(s.children + (name -> a)),
getter = s => s.children.get(name)
)
210 / 266

Configuration optics

def sectionChild(name: String) = Optional[Section, Config](
setter = (a, s) => Section(s.children + (name -> a)),
getter = s => s.children.get(name)
)
211 / 266

Configuration optics

def sectionChild(name: String) = Optional[Section, Config](
setter = (a, s) => Section(s.children + (name -> a)),
getter = s => s.children.get(name)
)
212 / 266

Configuration optics

def sectionChild(name: String) = Optional[Section, Config](
setter = (a, s) => Section(s.children + (name -> a)),
getter = s => s.children.get(name)
)
213 / 266

Configuration optics

def sectionChild(name: String) = Optional[Section, Config](
setter = (a, s) => Section(s.children + (name -> a)),
getter = s => s.children.get(name)
)
214 / 266

Configuration optics

def sectionChild(name: String) = Optional[Section, Config](
setter = (a, s) => Section(s.children + (name -> a)),
getter = s => s.children.get(name)
)
215 / 266

Configuration optics

def sectionChild(name: String) = Optional[Section, Config](
setter = (a, s) => Section(s.children + (name -> a)),
getter = s => s.children.get(name)
)
216 / 266

Configuration optics

def sectionChild(name: String) = Optional[Section, Config](
setter = (a, s) => Section(s.children + (name -> a)),
getter = s => s.children.get(name)
)
217 / 266

Configuration optics

def sectionChild(name: String) = Optional[Section, Config](
setter = (a, s) => Section(s.children + (name -> a)),
getter = s => s.children.get(name)
)
218 / 266

Configuration optics

val identityOpt = Optional[Config, Config](
setter = (a, _) => a,
getter = s => Some(s)
)
219 / 266

Configuration optics

val identityOpt = Optional[Config, Config](
setter = (a, _) => a,
getter = s => Some(s)
)
220 / 266

Configuration optics

val identityOpt = Optional[Config, Config](
setter = (a, _) => a,
getter = s => Some(s)
)
221 / 266

Configuration optics

val identityOpt = Optional[Config, Config](
setter = (a, _) => a,
getter = s => Some(s)
)
222 / 266

ConfigPath

case class ConfigPath(current: Optional[Config, Config]) {
val asValue = composeOP(current, value)
val asSection = composeOP(current, section)
def child(name: String) = ConfigPath(
composeOO(
asSection,
sectionChild(name)
)
)
}
223 / 266

ConfigPath

case class ConfigPath(current: Optional[Config, Config]) {
val asValue = composeOP(current, value)
val asSection = composeOP(current, section)
def child(name: String) = ConfigPath(
composeOO(
asSection,
sectionChild(name)
)
)
}
224 / 266

ConfigPath

case class ConfigPath(current: Optional[Config, Config]) {
val asValue = composeOP(current, value)
val asSection = composeOP(current, section)
def child(name: String) = ConfigPath(
composeOO(
asSection,
sectionChild(name)
)
)
}
225 / 266

ConfigPath

case class ConfigPath(current: Optional[Config, Config]) {
val asValue = composeOP(current, value)
val asSection = composeOP(current, section)
def child(name: String) = ConfigPath(
composeOO(
asSection,
sectionChild(name)
)
)
}
226 / 266

ConfigPath

case class ConfigPath(current: Optional[Config, Config]) {
val asValue = composeOP(current, value)
val asSection = composeOP(current, section)
def child(name: String) = ConfigPath(
composeOO(
asSection,
sectionChild(name)
)
)
}
227 / 266

ConfigPath

case class ConfigPath(current: Optional[Config, Config]) {
val asValue = composeOP(current, value)
val asSection = composeOP(current, section)
def child(name: String) = ConfigPath(
composeOO(
asSection,
sectionChild(name)
)
)
}
228 / 266

ConfigPath

case class ConfigPath(current: Optional[Config, Config]) {
val asValue = composeOP(current, value)
val asSection = composeOP(current, section)
def child(name: String) = ConfigPath(
composeOO(
asSection,
sectionChild(name)
)
)
}
229 / 266

ConfigPath

case class ConfigPath(current: Optional[Config, Config]) {
val asValue = composeOP(current, value)
val asSection = composeOP(current, section)
def child(name: String) = ConfigPath(
composeOO(
asSection,
sectionChild(name)
)
)
}
230 / 266

ConfigPath

val classifierName: Optional[Config, Value] =
ConfigPath(identityOpt).
child("classifier").
child("name").
asValue
231 / 266

ConfigPath

val classifierName: Optional[Config, Value] =
ConfigPath(identityOpt).
child("classifier").
child("name").
asValue
232 / 266

ConfigPath

val classifierName: Optional[Config, Value] =
ConfigPath(identityOpt).
child("classifier").
child("name").
asValue
233 / 266

ConfigPath

val classifierName: Optional[Config, Value] =
ConfigPath(identityOpt).
child("classifier").
child("name").
asValue
234 / 266

ConfigPath

val classifierName: Optional[Config, Value] =
ConfigPath(identityOpt).
child("classifier").
child("name").
asValue
235 / 266

ConfigPath

val classifierName: Optional[Config, Value] =
ConfigPath(identityOpt).
child("classifier").
child("name").
asValue
236 / 266

Dynamic

import scala.language.dynamics
object UpCase extends Dynamic {
def selectDynamic(missingMember: String): String =
missingMember.toUpperCase
}
UpCase.bar
237 / 266

Dynamic

import scala.language.dynamics
object UpCase extends Dynamic {
def selectDynamic(missingMember: String): String =
missingMember.toUpperCase
}
UpCase.bar
238 / 266

Dynamic

import scala.language.dynamics
object UpCase extends Dynamic {
def selectDynamic(missingMember: String): String =
missingMember.toUpperCase
}
UpCase.bar
239 / 266

Dynamic

import scala.language.dynamics
object UpCase extends Dynamic {
def selectDynamic(missingMember: String): String =
missingMember.toUpperCase
}
UpCase.bar
240 / 266

Dynamic

import scala.language.dynamics
object UpCase extends Dynamic {
def selectDynamic(missingMember: String): String =
missingMember.toUpperCase
}
UpCase.bar
241 / 266

Dynamic

import scala.language.dynamics
object UpCase extends Dynamic {
def selectDynamic(missingMember: String): String =
missingMember.toUpperCase
}
UpCase.bar
242 / 266

Dynamic

import scala.language.dynamics
object UpCase extends Dynamic {
def selectDynamic(missingMember: String): String =
missingMember.toUpperCase
}
UpCase.bar
243 / 266

Dynamic

import scala.language.dynamics
object UpCase extends Dynamic {
def selectDynamic(missingMember: String): String =
missingMember.toUpperCase
}
UpCase.bar
// res5: String = BAR
244 / 266

Dynamic ConfigPath

case class ConfigPath(
current: Optional[Config, Config]
) extends Dynamic {
val asValue = composeOP(current, value)
val asSection = composeOP(current, section)
def selectDynamic(child: String) = ConfigPath(
composeOO(
asSection,
sectionChild(child)
)
)
}
245 / 266

Dynamic ConfigPath

case class ConfigPath(
current: Optional[Config, Config]
) extends Dynamic {
val asValue = composeOP(current, value)
val asSection = composeOP(current, section)
def selectDynamic(child: String) = ConfigPath(
composeOO(
asSection,
sectionChild(child)
)
)
}
246 / 266

Dynamic ConfigPath

case class ConfigPath(
current: Optional[Config, Config]
) extends Dynamic {
val asValue = composeOP(current, value)
val asSection = composeOP(current, section)
def selectDynamic(child: String) = ConfigPath(
composeOO(
asSection,
sectionChild(child)
)
)
}
247 / 266

Dynamic ConfigPath

val classifierName =
ConfigPath(identityOpt).
classifier.
name.
asValue
248 / 266

Dynamic ConfigPath

val classifierName =
ConfigPath(identityOpt).
classifier.
name.
asValue
249 / 266

Dynamic ConfigPath

val classifierName =
ConfigPath(identityOpt).
classifier.
name.
asValue
250 / 266

Dynamic ConfigPath

val classifierName =
ConfigPath(identityOpt).
classifier.
name.
asValue
251 / 266

Dynamic ConfigPath

val classifierName =
ConfigPath(identityOpt).
classifier.
name.
asValue
252 / 266

Dynamic ConfigPath

val classifierName =
ConfigPath(identityOpt).
classifier.
name.
asValue
253 / 266

Dynamic ConfigPath

val root = ConfigPath(identityOpt)
254 / 266

Dynamic ConfigPath

val classifierName =
root.
classifier.
name.
asValue
255 / 266

Dynamic ConfigPath

classifierName.get(conf)
// res6: Option[Value] = Some(Value(news20))
256 / 266

Key takeaway

257 / 266

Key takeaway

Optics work not only with ADTs, but with any nested immutable data structure.

258 / 266

In closing

259 / 266

If you only remember 1 slide...

260 / 266

If you only remember 1 slide...

  • Lenses are used to drill down arbitrarily deep in nested product types.
261 / 266

If you only remember 1 slide...

  • Lenses are used to drill down arbitrarily deep in nested product types.
  • Prisms are used to explore sum types.
262 / 266

If you only remember 1 slide...

  • Lenses are used to drill down arbitrarily deep in nested product types.
  • Prisms are used to explore sum types.
  • They compose into optionals.
263 / 266

If you only remember 1 slide...

  • Lenses are used to drill down arbitrarily deep in nested product types.
  • Prisms are used to explore sum types.
  • They compose into optionals.
  • You can use optics to navigate any immutable nested data structure.
264 / 266

Questions?

Nicolas Rinaudo • @NicolasRinaudoBesedo

265 / 266

Bonus Material!

266 / 266

Algebraic Data Types

2 / 266
Paused

Help

Keyboard shortcuts

, , Pg Up, k Go to previous slide
, , Pg Dn, Space, j Go to next slide
Home Go to first slide
End Go to last slide
Number + Return Go to specific slide
b / m / f Toggle blackout / mirrored / fullscreen mode
c Clone slideshow
p Toggle presenter mode
t Restart the presentation timer
?, h Toggle this help
Esc Back to slideshow