Customization
Two knobs change how a registry resolves: refinements override what
gets returned for a given type when the resolver is on a particular path
through the graph, and erase drops type-level tracking entirely. A
third pattern — wrapping a registered value with a transforming fun —
isn’t a separate combinator but worth calling out below.
Transforming a value with a wrapping fun
Need to post-process a registered value? Prepend a fun(t: T => T) above
an existing T producer. LIFO selects the wrapper first; its recursive
T input pulls the underlying value from below; the wrapper transforms
it.
import registry.*
case class Greeting(text: String)
val r =
fun[Greeting] +:
fun((s: String) => s.toUpperCase) +:
value("hello")
r.make[Greeting]
// res0: Greeting = Greeting("HELLO")
Multiple wrappers compose by stacking — innermost runs first:
val n =
fun((x: Int) => x * 2) +: // 43 -> 86
fun((x: Int) => x + 1) +: // 42 -> 43
value(42)
// n: Registry[*:[Int, *:[Int, EmptyTuple]], *:[Int, *:[Int, *:[Int, EmptyTuple]]]] = Registry(
// entries = List(
// Basic(
// inputs = List(Int),
// output = Int,
// invoke = repl.MdocSession$MdocApp$$Lambda/0x00007f92d9a205f0@7a5abd14,
// fresh = false,
// resetFn = registry.Entry$Basic$$$Lambda/0x00007f92d9a1a768@4d80783a
// ),
// Basic(
// inputs = List(Int),
// output = Int,
// invoke = repl.MdocSession$MdocApp$$Lambda/0x00007f92d9a209c0@546f6c73,
// fresh = false,
// resetFn = registry.Entry$Basic$$$Lambda/0x00007f92d9a1a768@4d80783a
// ),
// Basic(
// inputs = List(),
// output = Int,
// invoke = registry.Fun$package$$$Lambda/0x00007f92d9a1b190@20977aa0,
// fresh = false,
// resetFn = registry.Entry$Basic$$$Lambda/0x00007f92d9a1a768@4d80783a
// )
// ),
// refinements = List()
// )
n.make[Int]
// res1: Int = 86
Within a single make call the wrapper is invoked once and shared across
all consumers (per-make cache; see Memoization). To opt
out, mark the wrapping entry .fresh.
refine[Path](v) — path-scoped override
When the resolution stack contains the types of Path as a subsequence
(in order, not necessarily contiguous) and the resolver is looking for
T, return v instead of doing the normal lookup.
The target type T is inferred from v, so you normally only specify the
path: refine[Server]("server-"). The explicit form
refine[Server, String]("server-") is still available when you want to
ascribe T.
Path may be a single type (the override fires whenever that type
appears anywhere on the stack) or a tuple of types (the override fires
only when all those types appear in order on the stack). One combinator,
both shapes — selected by a PathTags match type.
Single-type Path
case class Logger(prefix: String)
case class Server(log: Logger)
case class Worker(log: Logger)
val app =
fun[Server] +:
fun[Worker] +:
fun[Logger] +:
value("default") +:
Registry.empty
By default both share the same Logger:
app.make[Server].log.prefix
// res2: String = "default"
app.make[Worker].log.prefix
// res3: String = "default"
refine[Server]("server-") overrides String only when the
resolution stack passes through Server:
val tagged = app.refine[Server]("server-")
tagged.make[Server].log.prefix
// res4: String = "server-"
tagged.make[Worker].log.prefix
// res5: String = "default"
Tuple Path
The resolution stack may contain Server for many reasons. To narrow
further, give a path of types that must all appear, in order, in the
stack.
case class Outer(s: Server)
val withOuter =
fun[Outer] +: app
val pathed = withOuter.refine[(Outer, Server)]("from-outer-")
pathed.make[Outer].s.log.prefix
// res6: String = "from-outer-"
pathed.make[Server].log.prefix // not reached via Outer — default wins
// res7: String = "default"
refine — refinements as standalone values
refine[Path](v) (top-level, not on a Registry) produces a
Refinement value that you can prepend with any of +:, *:, -:. It’s
the same machinery as the refine method on Registry but expressed as
a value, which is sometimes cleaner when assembling registries from parts.
val refined =
refine[Server]("from-refinement-") +: app
refined.make[Server].log.prefix
// res8: String = "from-refinement-"
A refinement adds no entries and doesn’t change the type-level
AllIns / AllOuts accounting — +:, *:, -: all behave identically
for it.
erase — drop type-level tracking
If you’ve assembled the registry through a series of *: chains and want to
hand it off as a “plain” registry without any AllIns / AllOuts
information, erase flattens it:
val erased: Registry[EmptyTuple, EmptyTuple] = r.erase
make still works on the erased registry; makeSafe can no longer prove
anything. Useful for storing assembled registries behind a uniform type,
e.g. a map keyed by environment name.