Fixpoints and dependencies
In NixOS, config is a fixpoint: every module reads the final config, and Nix laziness computes each value
when something reads it. A yen fleet is a fixpoint too, in two layers:
- Declarations. Every module gets
yen, which is computed from the declarations of all modules, its own too. A module reads the data of any file throughyen, as a NixOS module readsconfig.x. - Results. An element reads the reduced value of a sink through an arg. That value can hold elements that read other sinks. Sinks feed sources and sources feed sinks; laziness closes the loop.
The order of the files and of the definitions never matters.
Each NixOS pattern, in yen
Section titled “Each NixOS pattern, in yen”This fleet has two hosts. Each sink plays the part of one NixOS option:
# Two hosts. Each sink is one NixOS option: many files give to it, the engine reduces it.{ yen, ... }:{ yen.kinds.fleet = { spawns.host = _: yen.host; sinks.addrs.reduce = yen.lib.reduce.merge; # (1) a fleet-level sink: every host gives, every host reads }; yen.kinds.host = { fields.addr = yen.lib.is builtins.isString; fields.fqdn = yen.lib.derive (yen.lib.is builtins.isString) (i: "${i.name}.lan"); # (2) a default from the instance sinks = { ports.reduce = yen.lib.reduce.set; firewall.reduce = yen.lib.reduce.one; settings.reduce = yen.lib.reduce.merge; motd.reduce = es: { # (3) the lowest priority number wins, like mkForce over mkDefault value = (builtins.head (builtins.sort (a: c: a.value.prio < c.value.prio) es)).value.text; errors = [ ]; }; peers.reduce = yen.lib.reduce.one; }; }; yen.fleet.main = { }; yen.flake = { addrs, ... }: { inherit addrs; }; # (4) the flake outputs read a sink too yen.host = { a.addr = "10.0.0.1"; b.addr = "10.0.0.2"; };}At (1), a sink of the fleet: every host gives to it, every host reads it. At (2), yen.lib.derive makes a default
from the instance, as a NixOS default reads config. At (3), a reduce you write picks one value. At (4), the flake
outputs read a sink too. The aspects give the elements:
# Each aspect is one NixOS pattern. None of them knows the files that read it.{ yen, ... }:{ yen.aspects = { fw = { at = yen.kinds.host; firewall = { ports, ... }: builtins.sort builtins.lessThan ports; # (1) reads another option: config.ports }; ssh = { at = yen.kinds.host; ports = 22; }; web = { at = yen.host.a; ports = 80; }; tls = { at = yen.aspects.web; # (2) mkIf: active where web is ports = 443; }; base = { at = yen.kinds.host; settings.log.level = "info"; motd = { prio = 1000; text = "welcome"; }; }; tuned = { at = yen.host.b; settings.log.file = "/var/log/b"; motd = { prio = 50; text = "maintenance"; }; }; addrs = { at = yen.kinds.host; addrs = # (3) every host gives its address to the fleet { host, ... }: { ${host.name} = host.addr; }; peers = { host, addrs, ... }: removeAttrs addrs [ host.name ]; # (4) the other nodes, through the fleet }; };}At (1), firewall reads the sink ports, as a NixOS option reads another option. At (2), at names an aspect,
so tls is active where web is. At (3) and (4), each host gives its address to the fleet, and peers reads
what the other hosts gave. This table maps each pattern to its name in this fleet:
| NixOS | yen | Here |
|---|---|---|
| a list option many modules define | a sink; each aspect gives an element | ports |
mkIf config.services.web.enable |
at refers to an aspect |
tls.at = yen.aspects.web |
| an option read from another option | a sink arg | firewall = { ports, ... }: … |
a default computed from config |
yen.lib.derive |
fqdn |
| attrsets merged from many modules | reduce.merge |
settings |
mkForce and mkDefault |
a reduce that picks by priority |
motd |
nodes.<other>.config |
give to a sink above both hosts, read it back | addrs, peers |
{ addrs = { "fleet:main" = { a = "10.0.0.1"; b = "10.0.0.2"; }; }; firewall = { "fleet:main/host:a" = [ 22 80 443 ]; "fleet:main/host:b" = [ 22 ]; }; motd = { "fleet:main/host:a" = "welcome"; "fleet:main/host:b" = "maintenance"; }; peers = { "fleet:main/host:a" = { b = "10.0.0.2"; }; "fleet:main/host:b" = { a = "10.0.0.1"; }; }; ports = { "fleet:main/host:a" = [ 22 443 80 ]; "fleet:main/host:b" = [ 22 ]; }; settings = { "fleet:main/host:a" = { log = { level = "info"; }; }; "fleet:main/host:b" = { log = { file = "/var/log/b"; level = "info"; }; }; };}Dependencies are edges
Section titled “Dependencies are edges”In NixOS, any module can read any option, so a dependency is a read hidden in a function. Nothing lists it, and “what changes if I change this?” has no answer before you evaluate.
In yen, an element reads only what reaches its scope: its own scope, the nearest scope of a kind, and the
sinks of its receivers. A value goes from one scope to another only as a routed element, and each routed
element is an edge of the graph, with the aspect that gave it. Here are the edges of the sink addrs.
peers on a reads what b gave to the fleet:
[ { from = "fleet:main/host:a"; origin = "aspect addrs"; sink = "addrs"; to = "fleet:main"; } { from = "fleet:main/host:b"; origin = "aspect addrs"; sink = "addrs"; to = "fleet:main"; }]Where routing helps
Section titled “Where routing helps”Because the edges are the dependencies, these questions are queries, not a search:
result.impact scope { }: the scopes a scope’s elements reach, by depth. See History.result.explain aspect scope depth: why an aspect is or is not active at a scope.- A grant limits the scopes and sinks a change may give to. It can check a change because every new dependency is a new edge. See Stage a change.
- Labels: an element carries the trust of the files that declare its aspect. See Agent surface.
A read of any value of any scope would add dependencies that are not edges, and these answers would miss
them. So yen has no such read. To read another host’s value, give it to a sink of a scope above both hosts,
as addrs does.
What yen does not do
Section titled “What yen does not do”NixOS resolves two values for one option with priorities: mkForce wins. yen never overrides data: two
different values for one declared leaf are an error, and you keep one. For values that many files give,
the sink’s reduce decides, as motd does above.
# A second, different value for one declared leaf. NixOS: mkForce wins. yen: two answers.{ yen, ... }:{ yen.host.a.addr = "10.0.0.9";}[ { code = "two-answers"; path = [ "host" "a" "addr" ]; }]