Generics
The cats-eo-generics module supplies two macros that eliminate
the boilerplate usually written by hand for Lens / Prism
derivation.
libraryDependencies += "dev.constructive" %% "cats-eo-generics" % "0.1"
import eo.optics.Optic.*
import eo.generics.{lens, prism}
import eo.docs.{Address, Customer, Person, Shape, Shape2, Coords, Zip}
Macro-derived optics need their target case classes and enum cases to live at a package-level location. The page hosts its samples ineo.docs.*for that reason — the samelens/prismcalls work identically on your own top-level ADTs.
lens[S](_.field)
Two-step partial application: lens[Customer] pins the source
type, the second call picks the field:
val nameL = lens[Customer](_.name)
val ageL = lens[Customer](_.age)
val alice = Customer("Alice", 30)
// alice: Customer = Customer(name = "Alice", age = 30)
nameL.get(alice)
// res0: String = "Alice"
ageL.replace(31)(alice)
// res1: Customer = Customer(name = "Alice", age = 31)
nameL.modify(_.toUpperCase)(alice)
// res2: Customer = Customer(name = "ALICE", age = 30)
Works on any N-field case class. The macro also handles Scala 3
enum cases, which would normally break under Monocle's
GenLens because enum cases don't expose .copy. EO emits a
direct new S(…) call through
hearth's
CaseClass.construct, which works uniformly for both.
Composition
Use .andThen to drill deeper:
val streetL =
lens[Person](_.address).andThen(lens[Address](_.street))
val bob = Person("Bob", Address("Elm St", Zip(54321, "0000")))
// bob: Person = Person(
// name = "Bob",
// address = Address(
// street = "Elm St",
// zip = Zip(code = 54321, extension = "0000")
// )
// )
streetL.get(bob)
// res3: String = "Elm St"
streetL.modify(_.toUpperCase)(bob)
// res4: Person = Person(
// name = "Bob",
// address = Address(
// street = "ELM ST",
// zip = Zip(code = 54321, extension = "0000")
// )
// )
Type-level complement
The derived lens exposes the structural complement of the
focused field as its existential X. For an N-field case class
focused on one field, X is a NamedTuple over the remaining
fields, preserving both names and types. That's the evidence
Optic.transform / .place / .transfer need — no
given at the call site required:
val renamed = nameL.place("Carol")(alice)
// renamed: Customer = Customer(name = "Carol", age = 30)
prism[S, A]
A prism[S, A] derives a Prism from the parent sum type S to
a specific child A <: S. Recognises:
- Scala 3 enums,
- Sealed traits with direct child types,
- Scala 3 union types.
val circleP = prism[Shape, Shape.Circle]
val squareP = prism[Shape, Shape.Square]
val triangleP = prism[Shape, Shape.Triangle]
circleP.to(Shape.Circle(1.0))
// res5: Either[X, Circle] = Right(Circle(1.0))
circleP.to(Shape.Square(2.0))
// res6: Either[X, Circle] = Left(Square(2.0))
circleP.modify(c => Shape.Circle(c.r * 2))(Shape.Circle(1.0))
// res7: Shape = Circle(2.0)
circleP.modify(c => Shape.Circle(c.r * 2))(Shape.Square(2.0))
// res8: Shape = Square(2.0)
Union types work the same way:
val intP = prism[Int | String, Int]
intP.to(42: Int | String)
// res9: Either[X, Int] = Right(42)
intP.to("hi": Int | String)
// res10: Either[X, Int] = Left("hi")
Composition with Lens chains
prism ∘ lens works naturally through Composer bridges:
import eo.data.Affine
val circleCoordsX =
prism[Shape2, Shape2.Circle]
.morph[Affine]
.andThen(lens[Shape2.Circle](_.c).morph[Affine])
.andThen(lens[Coords](_.x).morph[Affine])
circleCoordsX.modify(_ + 10)(Shape2.Circle(Coords(3, 4), 1.0))
// res11: Shape2 = Circle(c = Coords(x = 13, y = 4), r = 1.0)
circleCoordsX.modify(_ + 10)(Shape2.Square(Coords(3, 4), 2.0))
// res12: Shape2 = Square(c = Coords(x = 3, y = 4), s = 2.0)
Macro errors
Both macros fail at compile time with explicit errors when their input doesn't fit. Examples:
lens[Person](_.address.street)— nested paths not yet supported in a single macro call; chain instead:lens[Person](_.address).andThen(lens[Address](_.street)).prism[Shape, OtherEnum.Foo]—Amust be a direct child ofS; otherwise the macro aborts with "not a direct child of".lens[NonCaseClass](_.field)— only works on case classes.
See the Scaladoc for
LensMacro
and
PrismMacro
for the implementation details.