Automatic Argument Injection
A server’s monitoring feature needs to know which host it runs on, so that it can publish a
scrape target such as web-1:9100. The feature is written once and used on many hosts. The
module asks for host in its argument list, and gen supplies the right host for each use.
let gen = inputs.gen.lib.mkGenLibs { }; m = gen.merge;
# A feature module that asks for the host it configures. monitoring = { host, ... }: { monitoring.target = "${host.name}:9100"; };
forWeb1 = gen.bind.wrap { bindings.host = { name = "web-1"; }; } monitoring;
web1 = m.evalModuleTree { } [ { options.monitoring.target = m.mkOption { type = m.types.str; }; } forWeb1.module ];inweb1.config.monitoring.target"web-1:9100"How it works
Section titled “How it works”wrap reads the argument names the module declares. For each name that has a binding, it passes
the bound value in. Arguments without a binding, such as config or lib, are left for the
module system to supply as usual. Bindings the module does not ask for are ignored, so one set of
bindings can serve every feature a host uses.
The wrapped result also describes the module’s interface. forWeb1.signature lists the arguments
that were bound and the ones still required, so a framework can check a module’s needs before it
evaluates anything. If a module asks for host and nothing binds it, evaluation fails with:
gen-merge: module argument `host' is not definedChecking what is passed in
Section titled “Checking what is passed in”A binding can carry a contract and a note of where its value came from:
gen.bind.wrap { bindings.host = { name = "web-1"; }; contracts.host = gen.bind.contract.hasFields [ "name" "system" ]; provenance.host = { source = "fleet"; scope = "host=web-1"; };} monitoringThe host value above has no system field, so reading it fails with a message that names the
missing field and the source:
gen-bind: contract violation: value must have fields: name, system(provided by 'fleet' at scope 'host=web-1')The contract runs when the module reads host. A module that never reads a binding never pays
for its check.
When a binding has the same name as an argument the module system already provides, such as
lib, the binding wins by default. You can choose per name to let the module system win or to
make the collision an error.
One value, any module system
Section titled “One value, any module system”A service api listens on port 8080. Its NixOS host, its cloud firewall rule and its Kubernetes
Service all need that port. Each target has its own module format, and the same binding reaches
all three.
let gen = inputs.gen.lib.mkGenLibs { }; m = gen.merge;
service = { name = "api"; port = 8080; };
# Three modules, each shaped for a different tool, all asking for `service`. forNixos = { service, ... }: { services.nginx.virtualHosts.${service.name}.listen = [ { port = service.port; } ]; }; forTerranix = { service, ... }: { resource.aws_security_group_rule.${service.name} = { from_port = service.port; to_port = service.port; }; }; forKubernetes = { service, ... }: { services.${service.name}.spec.ports = [ { port = service.port; } ]; };
# Evaluate one module with the value bound. The freeform type accepts any shape. render = module: (m.evalModuleTree { } [ { freeformType = m.types.lazyAttrsOf m.types.anything; } (gen.bind.wrap { bindings = { inherit service; }; } module).module ]).config;in{ nixos = render forNixos; terranix = render forTerranix; kubernetes = render forKubernetes;}{ kubernetes = { services.api.spec.ports = [ { port = 8080; } ]; }; nixos = { services.nginx.virtualHosts.api.listen = [ { port = 8080; } ]; }; terranix = { resource.aws_security_group_rule.api = { from_port = 8080; to_port = 8080; }; }; }gen composes a configuration in its own evaluation, without nixpkgs, and passes the target tool
plain values such as service. gen’s own types stay on gen’s side, so the target never has to
understand them. This boundary is a declared interim: the target’s own checks on these values are
off by default, and each place a value is handed over without that check is recorded as a declared
opt-out. Full nixpkgs is loaded only where a target needs it, for example when building a NixOS
system.
The example above evaluates each module with gen’s engine to show the values. In a framework, a terminal hands the same wrapped modules to the target’s own evaluator (Delivery). The adios module system uses the same value-passing boundary.
The theory
Section titled “The theory”An implicit parameter is a value a function uses without every caller passing it by hand. The nearest enclosing binding supplies it. Lewis et al. showed how to give such parameters static types, so a function’s type records which implicit values it needs. A gen module’s argument list plays the same role: it declares what the module needs, and the framework binds it where the module is used. See our reading of Lewis et al. (2000).
Next: Rules That Add Relationships.
Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).