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.
Lenses provide bidirectional access to parts of data structures, enabling both getting and setting values in a composable way.
// Core lens trait for accessing field A in structure S
trait Lens[S, A] extends LPLens[S, A] {
def get(s: S): A
def set(s: S)(a: A): S
// Transform the focused value using function f
def modify(s: S)(f: A => A): S = set(s)(f(get(s)))
// Compose with another lens
def compose[T](g: Lens[T, S]): Lens[T, A]
// Compose with a prism
def compose[T](g: Prism[T, S]): Prism[T, A]
// Navigate to nth field
def >>(n: Nat)(implicit mkLens: MkNthFieldLens[A, n.N]): Lens[S, mkLens.Elem]
// Navigate to named field
def >>(k: Witness)(implicit mkLens: MkFieldLens[A, k.T]): Lens[S, mkLens.Elem]
// Pattern match extraction
def unapply(s: S): Option[A] = Some(get(s))
}Prisms provide partial access to sum types and optional values, handling cases where the focused value may not be present.
// Core prism trait for partial access to A within S
trait Prism[S, A] extends LPPrism[S, A] {
def get(s: S): Option[A]
def set(s: S)(a: A): S
// Transform the focused value if present
def modify(s: S)(f: A => A): S = get(s).map(f).map(a => set(s)(a)).getOrElse(s)
// Compose with lens
def compose[T](g: Lens[T, S]): Prism[T, A]
// Compose with another prism
def compose[T](g: Prism[T, S]): Prism[T, A]
// Dynamic field selection
def selectDynamic(k: String)(implicit mkPrism: MkSelectDynamicOptic[Prism[S, A], A, Symbol @@ k.type, Nothing]): mkPrism.Out
// Constructor pattern matching
def apply[B](implicit mkPrism: MkCtorPrism[A, B]): Prism[S, B]
}
// Optional type for lens-prism composition
type Optional[S, A] = Prism[S, A]Dynamic path construction for navigating nested structures.
object Path {
// Start path construction
def apply[S]: PathBuilder[S, S]
// Path builder for composing optics
trait PathBuilder[S, A] {
def selectField[B](field: Witness.Aux[Symbol]): PathBuilder[S, B]
def selectIndex[B](index: Nat): PathBuilder[S, B]
def selectType[B]: PathBuilder[S, B]
}
}
// OpticDefns provides construction utilities
object OpticDefns {
// Create lens to field
def lens[S]: MkFieldLens[S]
// Create prism for coproduct injection
def prism[S]: MkCoproductPrism[S]
// Generic optic construction
def optic[S]: GenericOptics[S]
}
// Aliases for convenience
val optic = OpticDefns.optic
val lens = OpticDefns.lens
val prism = OpticDefns.prismType classes for automatically deriving lenses and prisms from generic representations.
// Generate lens for accessing field K in type S
trait MkFieldLens[S, K] {
type A
def apply(): Lens[S, A]
}
// Generate lens for nth element of HList
trait MkHListNthLens[L <: HList, N <: Nat] {
type A
def apply(): Lens[L, A]
}
// Generate prism for coproduct injection
trait MkCoproductPrism[C <: Coproduct, T] {
def apply(): Prism[C, T]
}
// Generate lens for generic product
trait MkGenericLens[S, T] {
def apply(): Lens[S, T]
}
// Usage:
case class Address(street: String, city: String, zip: String)
val streetLens = MkFieldLens[Address, 'street].apply()
val address = Address("123 Main St", "Anytown", "12345")
val street = streetLens.get(address) // "123 Main St"Higher-level operations and combinators for working with optics.
// Traverse operation for collections
trait MkTraversal[S, T] {
def apply(): Traversal[S, T]
}
// Iso for bidirectional transformations
case class Iso[S, A](get: S => A, set: A => S) {
def reverse: Iso[A, S] = Iso(set, get)
def >>[B](other: Iso[A, B]): Iso[S, B]
}
// Fold for reading multiple values
trait Fold[S, A] {
def foldMap[M: Monoid](s: S)(f: A => M): M
def toList(s: S): List[A]
}
// Setter for write-only access
trait Setter[S, A] {
def set(s: S, a: A): S
def modify(s: S)(f: A => A): S
}import shapeless._, lens._
case class Person(name: String, age: Int, address: Address)
case class Address(street: String, city: String, zip: String)
val person = Person("Alice", 30, Address("123 Main St", "Boston", "02101"))
// Create lenses
val nameLens = lens[Person] >> 'name
val ageLens = lens[Person] >> 'age
val streetLens = lens[Person] >> 'address >> 'street
// Get values
val name = nameLens.get(person) // "Alice"
val age = ageLens.get(person) // 30
val street = streetLens.get(person) // "123 Main St"
// Set values
val renamed = nameLens.set(person, "Alicia")
val older = ageLens.modify(person)(_ + 1)
val moved = streetLens.set(person, "456 Oak Ave")
println(renamed) // Person("Alicia", 30, Address("123 Main St", "Boston", "02101"))
println(older) // Person("Alice", 31, Address("123 Main St", "Boston", "02101"))sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(base: Double, height: Double) extends Shape
val shapes: List[Shape] = List(
Circle(5.0),
Rectangle(10.0, 20.0),
Circle(3.0),
Triangle(8.0, 12.0)
)
// Create prisms for each case
val circlePrism = prism[Shape] >> 'Circle
val rectanglePrism = prism[Shape] >> 'Rectangle
// Extract specific shapes
val circles = shapes.flatMap(circlePrism.get)
// List(Circle(5.0), Circle(3.0))
val rectangles = shapes.flatMap(rectanglePrism.get)
// List(Rectangle(10.0, 20.0))
// Modify specific shapes
val scaledShapes = shapes.map { shape =>
circlePrism.modify(shape)(circle => circle.copy(radius = circle.radius * 2))
}case class Company(name: String, employees: List[Employee])
case class Employee(name: String, role: Role, contact: Contact)
case class Role(title: String, department: String, level: Int)
case class Contact(email: String, phone: String)
val company = Company("TechCorp", List(
Employee("Bob", Role("Developer", "Engineering", 3), Contact("bob@tech.com", "555-1001")),
Employee("Carol", Role("Manager", "Engineering", 5), Contact("carol@tech.com", "555-1002"))
))
// Deep lens composition
val firstEmployeeEmailLens =
lens[Company] >> 'employees >>
lens[List[Employee]] >> at(0) >>
lens[Employee] >> 'contact >>
lens[Contact] >> 'email
// Access deeply nested field
val firstEmail = firstEmployeeEmailLens.get(company) // "bob@tech.com"
// Update deeply nested field
val updatedCompany = firstEmployeeEmailLens.set(company, "robert@tech.com")// Working with optional values
case class User(name: String, profile: Option[Profile])
case class Profile(bio: String, avatar: Option[String])
val user = User("Dave", Some(Profile("Software engineer", None)))
// Lens to optional field
val profileLens = lens[User] >> 'profile
val bioLens = profileLens >>? lens[Profile] >> 'bio
// Get optional bio
val bio = bioLens.get(user) // Some("Software engineer")
// Update bio if profile exists
val updatedUser = bioLens.modify(user)(_ + " at TechCorp")
// Handle missing profile
val userWithoutProfile = User("Eve", None)
val noBio = bioLens.get(userWithoutProfile) // None
val stillNone = bioLens.modify(userWithoutProfile)(_ + " update") // No change// Traverse collections with optics
case class Team(name: String, members: List[Person])
val team = Team("Alpha", List(
Person("Alice", 30, Address("123 Main", "Boston", "02101")),
Person("Bob", 25, Address("456 Oak", "Boston", "02102"))
))
// Create traversal for all team member ages
val memberAgesTraversal = lens[Team] >> 'members >> each >> 'age
// Get all ages
val ages = memberAgesTraversal.toList(team) // List(30, 25)
// Increment all ages
val olderTeam = memberAgesTraversal.modify(team)(_ + 1)
// All team members now one year older
// Filter and modify
val filteredUpdate = team.copy(
members = team.members.map { person =>
if (person.age > 28) ageLens.modify(person)(_ + 1) else person
}
)// Dynamic optic construction
def updateField[S, A](obj: S, fieldName: String, newValue: A)(implicit
lens: MkFieldLens[S, fieldName.type]
): S = {
val fieldLens = lens.apply()
fieldLens.set(obj, newValue)
}
// Usage with compile-time field name verification
val person = Person("Frank", 40, Address("789 Pine", "Seattle", "98101"))
val updated = updateField(person, "age", 41)
// Compile error if field doesn't exist// Lens laws - get/set coherence
def validateLens[S, A](lens: Lens[S, A], obj: S): Boolean = {
val original = lens.get(obj)
val restored = lens.set(obj, original)
// Law 1: get(set(obj, value)) == value
val setValue = 42.asInstanceOf[A]
val law1 = lens.get(lens.set(obj, setValue)) == setValue
// Law 2: set(obj, get(obj)) == obj
val law2 = lens.set(obj, lens.get(obj)) == obj
// Law 3: set(set(obj, a), b) == set(obj, b)
val law3 = {
val a = original
val b = setValue
lens.set(lens.set(obj, a), b) == lens.set(obj, b)
}
law1 && law2 && law3
}
// Validate lens correctness
val isValid = validateLens(nameLens, person) // true