cats-eo

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:

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 onlyEnv, 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:

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.