Registry and entries
A Registry is a list of entries. An entry knows what types it
consumes (its inputs), what type it produces (its output), and how to combine
the inputs into a value of the output. Building a value of type T means
finding an entry that produces T and recursively building each of its
inputs.
Entries
Three factories cover the typical cases:
import registry.*
case class Host(value: String)
case class Port(value: Int)
case class DbConfig(host: Host, port: Port)
// fun[T] — register T's primary constructor.
val dbConfigEntry = fun[DbConfig]
// fun(f) — register a function value, lambda, or eta-expanded reference.
val portEntry = fun((n: Int) => Port(n))
// value(x) — register a constant (zero inputs).
val hostEntry = value(Host("localhost"))
val rawPort = value(5432)
Each factory returns a TypedEntry[Ins, Out] — a thin wrapper that carries
the input tuple and output type at the type level. At runtime an entry is
just (inputs, output, invoke) keyed by izumi-reflect tags; the
phantom type info exists only to power the compile-time-checked combinators.
Building a registry
Entries are joined with prepend operators. There are four of them, in decreasing strictness:
+: — strict prepend
The compiler checks that every input on the left side is produced by something on the right. This forces bottom-up construction: leaves first.
val r =
fun[DbConfig] +:
value(Host("localhost")) +:
value(Port(5432))
If you flip the order — leaf-consumer-leaf — +: rejects the program at
compile time. (See Safety.)
*: — tracked prepend
Same type-level accounting as +:, but no compile-time check at the prepend
site. Useful while you’re sketching; defer the check to makeSafe:
val tracked =
fun[DbConfig] *:
value(Host("localhost")) *:
value(Port(5432))
-: — untracked prepend
Adds the entry at runtime but does not update the type-level
AllIns / AllOuts. The entry is invisible to makeSafe. Escape hatch when
you genuinely need to register something dynamically.
val withDynamicOverride =
value(Host("override")) -: r
<+> — merge
Combine two registries. The left operand’s entries come first, so on duplicate outputs the left wins (consistent with the LIFO rule below).
val left =
fun[DbConfig] +:
value(Host("left")) +:
value(Port(5432))
val right =
fun[DbConfig] +:
value(Host("right")) +:
value(Port(9999))
val merged = left <+> right
merged.make[Host] // left wins
// res0: Host = Host("left")
LIFO precedence
When two entries produce the same type, the one registered closest to the
head of the chain wins. Read +: as cons for registries — the head is
the most recently prepended entry.
val firstWins =
value(Host("override")) +: // head — wins for Host
fun[DbConfig] +:
value(Host("default")) +: // shadowed
value(Port(5432))
firstWins.make[Host]
// res1: Host = Host("override")
Resolving values
Two extractors:
r.make[DbConfig] // runtime resolution
// res2: DbConfig = DbConfig(host = Host("localhost"), port = Port(5432))
r.makeSafe[DbConfig] // compile-time-checked
// res3: DbConfig = DbConfig(host = Host("localhost"), port = Port(5432))
make[T] throws on missing dependencies or cycles; makeSafe[T] refuses to
compile when T is not produced or some declared input is uncovered. Both
are covered in detail in Resolution and
Safety.