Kyo integration
The cats-eo-kyo module integrates eo with
Kyo's dependency management
at the same three seams as the ZIO module — the two systems'
DI substrates are both type-indexed maps, and the integration surface
mirrors it deliberately:
TypeMap[R](whatEnv[R]carries, whatEnv.runAllaccepts) is a type-indexed map, so every tagged slot is a lawful Lens (service).Layer.fromtakes a wiring functionS => A < *— exactlyCanGet[S, A]— so aggregate services project to sub-service layers (Layer.focus).Var[S]state gets the same capability-driven focus ops aszio.Ref(Var.getFocus/updateFocus/setFocus).
Kyo's surface is companion-static (Env.get, Var.update), so the
focus ops here extend the companion objects and read like native Kyo.
The module depends on kyo-prelude only — Env, Var, Layer and
TypeMap all live in Kyo's dependency-light pure layer (kyo-data and
kyo-kernel come transitively). No kyo-core IO runtime is pulled in, so
the module is usable from pure kyo-prelude programs and full kyo-core
applications alike.
libraryDependencies += "dev.constructive" %% "cats-eo-kyo" % "0.13"
Which direction are you integrating?
The module is a two-way adapter; pick the row that matches what you are holding and what the other side expects:
| You're holding | The other side expects | Reach for |
|---|---|---|
an optic (lens[Config](_.maxOrders)) |
an Env read (Dependencies) |
Env.focus instead of an Env.use projection lambda |
| an optic | a Layer in the wiring graph (Wiring with layers) |
Layer.focus — the optic becomes the layer's wiring function |
| an optic | a TypeMap for Env.runAll overrides |
service + .andThen |
a TypeMap, Maybe, Result, or Var state |
eo capability evidence (CanGet[T, A], CanModify[T, A], …) |
import dev.constructive.eo.kyo.given — no hand-written given |
The service lens
service[R, A] focuses the A slot inside a TypeMap[R] — the
direct analogue of the ZIO module's ZEnvironment service lens, and
lawful for the same reason: add on a present tag replaces exactly
that entry, sibling slots are the untouched leftover.
import kyo.*
import dev.constructive.eo.*
import dev.constructive.eo.kyo.*
import dev.constructive.eo.generics.lens
case class Db(url: String, pool: Int)
case class Metrics(prefix: String)
val tm = TypeMap(Db("jdbc:h2", 4), Metrics("eo"))
val dbL = service[Db & Metrics, Db]
dbL.get(tm)
// res0: Db = Db(url = "jdbc:h2", pool = 4)
It composes with field optics through the ordinary .andThen, which
is what you want for Env.runAll test overrides — tweak one field of
one service in an environment map, leave the rest alone:
val poolL = lens[Db](_.pool)
// poolL: SimpleLens[Db, Int, NamedTuple[*:["url", EmptyTuple], *:[String, EmptyTuple]]] = dev.constructive.eo.optics.SimpleLens@3d995aca
dbL.andThen(poolL).modify(_ * 2)(tm).get[Db]
// res1: Db = Db(url = "jdbc:h2", pool = 8)
Env and Layer focus
Env.focus[R, A] reads a focus out of the R service in the
environment — Env.use routed through CanGet instead of an ad-hoc
projection lambda:
val urlL = lens[Db](_.url)
given CanGet[Db, String] = urlL
Env.run(Db("jdbc:h2", 4))(Env.focus[Db, String]).eval
// res2: String = "jdbc:h2"
Layer.focus[S, A] derives an A service layer from an S service
by focusing — Layer.from is CanGet-shaped, so the optic is the
wiring. It slots straight into Env.runLayer graphs:
val prog = Env.runLayer(Layer(Db("jdbc:h2", 4)), Layer.focus[Db, String])(Env.get[String])
// prog: <[String, Memo] = Kyo(kyo.Var[kyo.Memo$package$.Memo$.Cache], Input(kyo.Var$internal$Get$@9459474), kyo.md:69:12, 2", 4)), Layer.focus[Db, String])(Env.get[String]))
Memo.run(prog).eval
// res3: String = "jdbc:h2"
Kyo docs examples through optics
The Dependencies section of the kyo-prelude docs reads a limit out of a config service with a projection lambda:
// from the kyo docs:
case class Config(maxOrders: Int, currency: String)
val limit: Int < Env[Config] =
Env.use[Config](_.maxOrders)
With an optic naming the field, the same value is Env.focus — and
the optic is reusable everywhere else the field is touched (writes,
Var updates, layer wiring), not just in this one lambda:
case class Config(maxOrders: Int, currency: String)
val maxOrdersL = lens[Config](_.maxOrders)
val currencyL = lens[Config](_.currency)
given CanGet[Config, Int] = maxOrdersL
val limit: Int < Env[Config] = Env.focus[Config, Int]
Env.run(Config(5, "EUR"))(limit).eval
// res4: Int = 5
The Wiring with layers section
builds a configLayer and derives further layers from it with
Layer.from. When the derived service is a focus of the aggregate,
Layer.focus replaces the hand-written projection — same graph, same
Env.runLayer resolution:
val configLayer: Layer[Config, Any] =
Layer(Config(maxOrders = 10, currency = "USD"))
val currencyLayer: Layer[String, Env[Config]] = {
given CanGet[Config, String] = currencyL
Layer.focus[Config, String]
}
Memo.run(Env.runLayer(configLayer, currencyLayer)(Env.get[String])).eval
// res5: String = "USD"
Var focus ops
Var[S] is Kyo's stateful effect; the focus ops mirror the ZIO
module's Ref extensions with the same names and the same capability
demands. A read-then-write is ONE CanModify in a single
Var.updateDiscard pass:
val update =
for
_ <- Var.updateFocus[Db, String](_.toUpperCase)(using urlL)
_ <- Var.setFocus[Db, Int](8)(using poolL)
url <- Var.getFocus[Db, String]
yield url
// update: <[String, Var[Db]] = Kyo(kyo.Var[repl.MdocSession$.MdocApp.Db], Input(dev.constructive.eo.kyo.KyoOptics$package$$anon$5@1480b860), kyo.md:133:12, yield url)
Var.runTuple(Db("jdbc:h2", 4))(update).eval
// res6: Tuple2[Db, String] = (Db(url = "JDBC:H2", pool = 8), "JDBC:H2")
getFocusOption (via CanGetOption) covers partial foci — Prism,
Optional and AffineFold evidence.
Automatic capability givens
Kyo's types can also provide eo capabilities by themselves, mirroring
the ZIO module's givens. A
given import (the * wildcard deliberately does not pull givens)
puts one optic given per pair in scope:
(TypeMap[R], A)for every tagged slotAofR— theservicelens;(Maybe[A], A)— aPresentprism;(Result[E, A], A)— a success prism; failures and panics pass through writes untouched.
import dev.constructive.eo.kyo.given
def report[T](t: T)(using g: CanGet[T, Db]): String = g.get(t).url
def bump[T](t: T)(using m: CanModify[T, Int]): T = m.modify(_ + 1)(t)
report(tm)
// res7: String = "jdbc:h2"
bump(Maybe(41))
// res8: Maybe[Int] = 42
bump(Result.fail("e"): Result[String, Int])
// res9: Result[String, Int] = Failure("e")
Coherence rule as everywhere in eo: these are THE optic givens for
their (S, A) pairs — don't declare competing ones.
Explicit nulls
Kyo's inline kernel is not -Yexplicit-nulls-clean: its machinery
expands inside your compilation units wherever a kyo inline def is
used (.eval, Env.run, …) and passes a null Safepoint
interceptor, which fails to typecheck under that flag. This is a
property of Kyo, not of this module — but it means projects enabling
-Yexplicit-nulls cannot currently compile kyo call sites, with or
without eo. (cats-eo-kyo itself opts out of the flag that the rest
of the eo build carries.)
Overhead
KyoDiBench pairs the ops above against their hand-written
equivalents in two tiers: pure TypeMap ops (tm.get / tm.add),
and full effectful round-trips (Env.focus vs Env.use,
Var.updateFocus vs Var.updateDiscard) where both sides pay Kyo's
suspend/handle/eval machinery and the pair isolates the capability
hop. Measured: allocation parity on the map tier (reads 0 B/op both
sides, drilled writes identical), and +8–16 B/op on the effect tier —
the per-call capability closure. See the
benchmarks page for the current sweep.