Cookbook
Runnable patterns for the questions that come up most often. Every fence is compiled by mdoc against the current library version.
import eo.optics.{Iso, Lens, Optic, Optional, Prism, Setter, Traversal}
import eo.optics.Optic.*
import eo.data.Forgetful.given // Accessor[Forgetful] — powers .get on Iso / Getter
Edit a deeply-nested field
Using hand-written Lenses (the
generics module provides the lens[S](_.field)
macro that generates this boilerplate):
case class Zip(code: Int, extension: String)
case class Address(street: String, zip: Zip)
case class Company(name: String, address: Address)
val extL =
Lens[Company, Address](_.address, (c, a) => c.copy(address = a))
.andThen(Lens[Address, Zip](_.zip, (a, z) => a.copy(zip = z)))
.andThen(Lens[Zip, String](_.extension, (z, e) => z.copy(extension = e)))
val initech = Company("Initech", Address("Main St", Zip(12345, "6789")))
// initech: Company = Company(
// name = "Initech",
// address = Address(
// street = "Main St",
// zip = Zip(code = 12345, extension = "6789")
// )
// )
extL.get(initech)
// res0: String = "6789"
extL.replace("0000")(initech)
// res1: Company = Company(
// name = "Initech",
// address = Address(
// street = "Main St",
// zip = Zip(code = 12345, extension = "0000")
// )
// )
Focus an Option field (Optional-shaped)
import eo.data.Affine
case class Setting(name: String, timeout: Option[Int])
val timeoutL = Optional[Setting, Setting, Int, Int, Affine](
getOrModify = s => s.timeout.toRight(s),
reverseGet = { case (s, t) => s.copy(timeout = Some(t)) },
)
timeoutL.modify(_ * 2)(Setting("a", Some(10)))
// res2: Setting = Setting(name = "a", timeout = Some(20))
timeoutL.modify(_ * 2)(Setting("a", None))
// res3: Setting = Setting(name = "a", timeout = None)
Compose a Lens with an Optional
Morph the Lens into the Optional's carrier so they share F:
case class AppRoot(setting: Setting)
val appTimeoutL =
Lens[AppRoot, Setting](_.setting, (a, s) => a.copy(setting = s))
.morph[Affine]
.andThen(timeoutL)
appTimeoutL.modify(_ * 2)(AppRoot(Setting("a", Some(5))))
// res4: AppRoot = AppRoot(Setting(name = "a", timeout = Some(10)))
appTimeoutL.modify(_ * 2)(AppRoot(Setting("a", None)))
// res5: AppRoot = AppRoot(Setting(name = "a", timeout = None))
Modify every element of a list
Traversal.each is the linear-time fast path:
import cats.instances.list.given
val eachInt = Traversal.each[List, Int, Int]
eachInt.modify(_ + 1)(List(1, 2, 3))
// res6: List[Int] = List(2, 3, 4)
eachInt.foldMap(identity[Int])(List(1, 2, 3)) // sum
// res7: Int = 6
eachInt.foldMap((_: Int) => 1)(List(1, 2, 3)) // count
// res8: Int = 3
Modify every element and continue through a field
Reach for Traversal.powerEach when the chain continues after
the traversal (see
Optics → Traversal for the cost
tradeoff):
import eo.data.PowerSeries
case class Dial(isMobile: Boolean, number: String)
case class Subscriber(phones: List[Dial])
val everyMobile =
Lens[Subscriber, List[Dial]](_.phones, (o, ps) => o.copy(phones = ps))
.morph[PowerSeries]
.andThen[Dial, Dial](Traversal.powerEach[List, Dial])
.andThen(
Lens[Dial, Boolean](_.isMobile, (p, m) => p.copy(isMobile = m))
.morph[PowerSeries]
)
everyMobile.modify(!_)(Subscriber(List(
Dial(isMobile = false, "555-0001"),
Dial(isMobile = true, "555-0002"),
)))
// res9: Subscriber = Subscriber(
// List(
// Dial(isMobile = true, number = "555-0001"),
// Dial(isMobile = false, number = "555-0002")
// )
// )
Branch into a sum type
enum Input:
case Click(x: Int, y: Int)
case Scroll(delta: Int)
val clickP = Prism[Input, Input.Click](
{
case c: Input.Click => Right(c)
case other => Left(other)
},
identity,
)
clickP.modify(c => Input.Click(c.x + 1, c.y))(Input.Click(10, 20))
// res10: Input = Click(x = 11, y = 20)
clickP.modify(c => Input.Click(c.x + 1, c.y))(Input.Scroll(5))
// res11: Input = Scroll(5)
Edit JSON without decoding
import eo.circe.codecPrism
import io.circe.Codec
import io.circe.syntax.*
import hearth.kindlings.circederivation.KindlingsCodecAsObject
case class UserAddress(street: String, zip: Int)
object UserAddress:
given Codec.AsObject[UserAddress] = KindlingsCodecAsObject.derive
case class SiteUser(name: String, address: UserAddress)
object SiteUser:
given Codec.AsObject[SiteUser] = KindlingsCodecAsObject.derive
val userStreet = codecPrism[SiteUser].address.street
val userJson = SiteUser("Alice", UserAddress("Main St", 12345)).asJson
// userJson: Json = JObject(
// object[name -> "Alice",address -> {
// "street" : "Main St",
// "zip" : 12345
// }]
// )
userStreet.modify(_.toUpperCase)(userJson).noSpacesSortKeys
// res12: String = "{\"address\":{\"street\":\"MAIN ST\",\"zip\":12345},\"name\":\"Alice\"}"
Apply a function that needs an effect
Both .modifyF and .modifyA lift an A => G[B] through an
optic. Use .modifyF when the carrier admits just Functor[G],
.modifyA when you need the full Applicative[G]:
case class Shopper(name: String, age: Int)
val shopperAgeL =
Lens[Shopper, Int](_.age, (s, a) => s.copy(age = a))
import cats.syntax.functor.*
import cats.instances.option.*
shopperAgeL.modifyF[Option](age => if age >= 0 then Some(age + 1) else None)(
Shopper("Alice", 30)
)
// res13: Option[Shopper] = Some(Shopper(name = "Alice", age = 31))
shopperAgeL.modifyF[Option](age => if age >= 0 then Some(age + 1) else None)(
Shopper("Alice", -1)
)
// res14: Option[Shopper] = None
Iso between equivalent shapes
case class UserTuple(name: String, age: Int)
case class UserRecord(name: String, age: Int)
val userIso =
Iso[UserTuple, UserTuple, UserRecord, UserRecord](
ut => UserRecord(ut.name, ut.age),
ur => UserTuple(ur.name, ur.age),
)
userIso.get(UserTuple("Alice", 30))
// res15: UserRecord = UserRecord(name = "Alice", age = 30)
userIso.reverseGet(UserRecord("Bob", 25))
// res16: UserTuple = UserTuple(name = "Bob", age = 25)
Derive a read-only view
import eo.optics.Getter
val nameInitial = Getter[Shopper, Char](_.name.head)
nameInitial.get(Shopper("Alice", 30))
// res17: Char = 'A'
Further reading
- Concepts — the theory behind the unified Optic trait and carriers.
- Optics reference — the full per-family tour.
- Generics — macro-derived Lens / Prism.
- Circe integration — cursor-backed JSON optics.
- Migrating from Monocle — a side- by-side translation guide.