or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-utilities.mdcoproduct-unions.mdgeneric-derivation.mdhlist-collections.mdindex.mdoptics-lenses.mdpoly-typelevel.mdrecords-fields.md

optics-lenses.mddocs/

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

```