0
# Optics - Lenses and Prisms
1
2
Optics in Shapeless provide functional programming patterns for composable data access and transformation. They enable type-safe navigation and modification of nested data structures with automatic derivation, offering an elegant alternative to manual field access and updates.
3
4
## Capabilities
5
6
### Lens Types
7
8
Lenses provide bidirectional access to parts of data structures, enabling both getting and setting values in a composable way.
9
10
```scala { .api }
11
// Core lens trait for accessing field A in structure S
12
trait Lens[S, A] extends LPLens[S, A] {
13
def get(s: S): A
14
def set(s: S)(a: A): S
15
16
// Transform the focused value using function f
17
def modify(s: S)(f: A => A): S = set(s)(f(get(s)))
18
19
// Compose with another lens
20
def compose[T](g: Lens[T, S]): Lens[T, A]
21
22
// Compose with a prism
23
def compose[T](g: Prism[T, S]): Prism[T, A]
24
25
// Navigate to nth field
26
def >>(n: Nat)(implicit mkLens: MkNthFieldLens[A, n.N]): Lens[S, mkLens.Elem]
27
28
// Navigate to named field
29
def >>(k: Witness)(implicit mkLens: MkFieldLens[A, k.T]): Lens[S, mkLens.Elem]
30
31
// Pattern match extraction
32
def unapply(s: S): Option[A] = Some(get(s))
33
}
34
```
35
36
### Prism Types
37
38
Prisms provide partial access to sum types and optional values, handling cases where the focused value may not be present.
39
40
```scala { .api }
41
// Core prism trait for partial access to A within S
42
trait Prism[S, A] extends LPPrism[S, A] {
43
def get(s: S): Option[A]
44
def set(s: S)(a: A): S
45
46
// Transform the focused value if present
47
def modify(s: S)(f: A => A): S = get(s).map(f).map(a => set(s)(a)).getOrElse(s)
48
49
// Compose with lens
50
def compose[T](g: Lens[T, S]): Prism[T, A]
51
52
// Compose with another prism
53
def compose[T](g: Prism[T, S]): Prism[T, A]
54
55
// Dynamic field selection
56
def selectDynamic(k: String)(implicit mkPrism: MkSelectDynamicOptic[Prism[S, A], A, Symbol @@ k.type, Nothing]): mkPrism.Out
57
58
// Constructor pattern matching
59
def apply[B](implicit mkPrism: MkCtorPrism[A, B]): Prism[S, B]
60
}
61
62
// Optional type for lens-prism composition
63
type Optional[S, A] = Prism[S, A]
64
```
65
66
### Path Construction
67
68
Dynamic path construction for navigating nested structures.
69
70
```scala { .api }
71
object Path {
72
// Start path construction
73
def apply[S]: PathBuilder[S, S]
74
75
// Path builder for composing optics
76
trait PathBuilder[S, A] {
77
def selectField[B](field: Witness.Aux[Symbol]): PathBuilder[S, B]
78
def selectIndex[B](index: Nat): PathBuilder[S, B]
79
def selectType[B]: PathBuilder[S, B]
80
}
81
}
82
83
// OpticDefns provides construction utilities
84
object OpticDefns {
85
// Create lens to field
86
def lens[S]: MkFieldLens[S]
87
88
// Create prism for coproduct injection
89
def prism[S]: MkCoproductPrism[S]
90
91
// Generic optic construction
92
def optic[S]: GenericOptics[S]
93
}
94
95
// Aliases for convenience
96
val optic = OpticDefns.optic
97
val lens = OpticDefns.lens
98
val prism = OpticDefns.prism
99
```
100
101
### Automatic Optic Derivation
102
103
Type classes for automatically deriving lenses and prisms from generic representations.
104
105
```scala { .api }
106
// Generate lens for accessing field K in type S
107
trait MkFieldLens[S, K] {
108
type A
109
def apply(): Lens[S, A]
110
}
111
112
// Generate lens for nth element of HList
113
trait MkHListNthLens[L <: HList, N <: Nat] {
114
type A
115
def apply(): Lens[L, A]
116
}
117
118
// Generate prism for coproduct injection
119
trait MkCoproductPrism[C <: Coproduct, T] {
120
def apply(): Prism[C, T]
121
}
122
123
// Generate lens for generic product
124
trait MkGenericLens[S, T] {
125
def apply(): Lens[S, T]
126
}
127
128
// Usage:
129
case class Address(street: String, city: String, zip: String)
130
val streetLens = MkFieldLens[Address, 'street].apply()
131
val address = Address("123 Main St", "Anytown", "12345")
132
val street = streetLens.get(address) // "123 Main St"
133
```
134
135
### Optic Operations
136
137
Higher-level operations and combinators for working with optics.
138
139
```scala { .api }
140
// Traverse operation for collections
141
trait MkTraversal[S, T] {
142
def apply(): Traversal[S, T]
143
}
144
145
// Iso for bidirectional transformations
146
case class Iso[S, A](get: S => A, set: A => S) {
147
def reverse: Iso[A, S] = Iso(set, get)
148
def >>[B](other: Iso[A, B]): Iso[S, B]
149
}
150
151
// Fold for reading multiple values
152
trait Fold[S, A] {
153
def foldMap[M: Monoid](s: S)(f: A => M): M
154
def toList(s: S): List[A]
155
}
156
157
// Setter for write-only access
158
trait Setter[S, A] {
159
def set(s: S, a: A): S
160
def modify(s: S)(f: A => A): S
161
}
162
```
163
164
## Usage Examples
165
166
### Basic Lens Operations
167
168
```scala
169
import shapeless._, lens._
170
171
case class Person(name: String, age: Int, address: Address)
172
case class Address(street: String, city: String, zip: String)
173
174
val person = Person("Alice", 30, Address("123 Main St", "Boston", "02101"))
175
176
// Create lenses
177
val nameLens = lens[Person] >> 'name
178
val ageLens = lens[Person] >> 'age
179
val streetLens = lens[Person] >> 'address >> 'street
180
181
// Get values
182
val name = nameLens.get(person) // "Alice"
183
val age = ageLens.get(person) // 30
184
val street = streetLens.get(person) // "123 Main St"
185
186
// Set values
187
val renamed = nameLens.set(person, "Alicia")
188
val older = ageLens.modify(person)(_ + 1)
189
val moved = streetLens.set(person, "456 Oak Ave")
190
191
println(renamed) // Person("Alicia", 30, Address("123 Main St", "Boston", "02101"))
192
println(older) // Person("Alice", 31, Address("123 Main St", "Boston", "02101"))
193
```
194
195
### Prism Operations with Sum Types
196
197
```scala
198
sealed trait Shape
199
case class Circle(radius: Double) extends Shape
200
case class Rectangle(width: Double, height: Double) extends Shape
201
case class Triangle(base: Double, height: Double) extends Shape
202
203
val shapes: List[Shape] = List(
204
Circle(5.0),
205
Rectangle(10.0, 20.0),
206
Circle(3.0),
207
Triangle(8.0, 12.0)
208
)
209
210
// Create prisms for each case
211
val circlePrism = prism[Shape] >> 'Circle
212
val rectanglePrism = prism[Shape] >> 'Rectangle
213
214
// Extract specific shapes
215
val circles = shapes.flatMap(circlePrism.get)
216
// List(Circle(5.0), Circle(3.0))
217
218
val rectangles = shapes.flatMap(rectanglePrism.get)
219
// List(Rectangle(10.0, 20.0))
220
221
// Modify specific shapes
222
val scaledShapes = shapes.map { shape =>
223
circlePrism.modify(shape)(circle => circle.copy(radius = circle.radius * 2))
224
}
225
```
226
227
### Nested Data Access
228
229
```scala
230
case class Company(name: String, employees: List[Employee])
231
case class Employee(name: String, role: Role, contact: Contact)
232
case class Role(title: String, department: String, level: Int)
233
case class Contact(email: String, phone: String)
234
235
val company = Company("TechCorp", List(
236
Employee("Bob", Role("Developer", "Engineering", 3), Contact("bob@tech.com", "555-1001")),
237
Employee("Carol", Role("Manager", "Engineering", 5), Contact("carol@tech.com", "555-1002"))
238
))
239
240
// Deep lens composition
241
val firstEmployeeEmailLens =
242
lens[Company] >> 'employees >>
243
lens[List[Employee]] >> at(0) >>
244
lens[Employee] >> 'contact >>
245
lens[Contact] >> 'email
246
247
// Access deeply nested field
248
val firstEmail = firstEmployeeEmailLens.get(company) // "bob@tech.com"
249
250
// Update deeply nested field
251
val updatedCompany = firstEmployeeEmailLens.set(company, "robert@tech.com")
252
```
253
254
### Optional and Partial Access
255
256
```scala
257
// Working with optional values
258
case class User(name: String, profile: Option[Profile])
259
case class Profile(bio: String, avatar: Option[String])
260
261
val user = User("Dave", Some(Profile("Software engineer", None)))
262
263
// Lens to optional field
264
val profileLens = lens[User] >> 'profile
265
val bioLens = profileLens >>? lens[Profile] >> 'bio
266
267
// Get optional bio
268
val bio = bioLens.get(user) // Some("Software engineer")
269
270
// Update bio if profile exists
271
val updatedUser = bioLens.modify(user)(_ + " at TechCorp")
272
273
// Handle missing profile
274
val userWithoutProfile = User("Eve", None)
275
val noBio = bioLens.get(userWithoutProfile) // None
276
val stillNone = bioLens.modify(userWithoutProfile)(_ + " update") // No change
277
```
278
279
### Collection Traversal
280
281
```scala
282
// Traverse collections with optics
283
case class Team(name: String, members: List[Person])
284
285
val team = Team("Alpha", List(
286
Person("Alice", 30, Address("123 Main", "Boston", "02101")),
287
Person("Bob", 25, Address("456 Oak", "Boston", "02102"))
288
))
289
290
// Create traversal for all team member ages
291
val memberAgesTraversal = lens[Team] >> 'members >> each >> 'age
292
293
// Get all ages
294
val ages = memberAgesTraversal.toList(team) // List(30, 25)
295
296
// Increment all ages
297
val olderTeam = memberAgesTraversal.modify(team)(_ + 1)
298
// All team members now one year older
299
300
// Filter and modify
301
val filteredUpdate = team.copy(
302
members = team.members.map { person =>
303
if (person.age > 28) ageLens.modify(person)(_ + 1) else person
304
}
305
)
306
```
307
308
### Dynamic Field Access
309
310
```scala
311
// Dynamic optic construction
312
def updateField[S, A](obj: S, fieldName: String, newValue: A)(implicit
313
lens: MkFieldLens[S, fieldName.type]
314
): S = {
315
val fieldLens = lens.apply()
316
fieldLens.set(obj, newValue)
317
}
318
319
// Usage with compile-time field name verification
320
val person = Person("Frank", 40, Address("789 Pine", "Seattle", "98101"))
321
val updated = updateField(person, "age", 41)
322
// Compile error if field doesn't exist
323
```
324
325
### Lens Laws and Validation
326
327
```scala
328
// Lens laws - get/set coherence
329
def validateLens[S, A](lens: Lens[S, A], obj: S): Boolean = {
330
val original = lens.get(obj)
331
val restored = lens.set(obj, original)
332
333
// Law 1: get(set(obj, value)) == value
334
val setValue = 42.asInstanceOf[A]
335
val law1 = lens.get(lens.set(obj, setValue)) == setValue
336
337
// Law 2: set(obj, get(obj)) == obj
338
val law2 = lens.set(obj, lens.get(obj)) == obj
339
340
// Law 3: set(set(obj, a), b) == set(obj, b)
341
val law3 = {
342
val a = original
343
val b = setValue
344
lens.set(lens.set(obj, a), b) == lens.set(obj, b)
345
}
346
347
law1 && law2 && law3
348
}
349
350
// Validate lens correctness
351
val isValid = validateLens(nameLens, person) // true
352
```