Skip to content

Sinks and reduce

A sink on a kind collects the elements that aspects give, and reduces them to one value. This host kind has one sink for each way to reduce. The root kind and the flake are in the example code.

modules/sinks.nix
# A sink collects elements and reduces them. `of` checks each element.
{ yen, lib, ... }:
let
inherit (yen.lib.reduce)
list
set
flat
one
merge
elements
;
sorted = es: {
value = lib.sort (a: b: a < b) (map (e: e.value) es);
errors = [ ];
}; # (1) your own reduce
in
{
yen.kinds.host.sinks = {
motd = {
# (2)
reduce = list;
of = yen.lib.str;
};
ports = {
# (3)
reduce = set;
of = yen.lib.port;
};
packages.reduce = flat; # (4)
admin.reduce = one; # (5)
nginx.reduce = merge; # (6)
owners.reduce = elements; # (7)
groups.reduce = sorted; # (8)
};
# every sink of a host, read as args, to one flake output
yen.aspects.report = {
at = yen.kinds.host;
out =
{
host,
motd,
ports,
packages,
admin,
nginx,
owners,
groups,
...
}:
{
${host.name} = {
inherit
motd
ports
packages
admin
nginx
groups
;
owners = map (e: e.from.kind) owners;
};
};
};
}

A sink has a reduce. (2) and (3) also have of, which checks each element. (4) to (8) use another reduce each. (1) is a function you write. The default reduce is list. The table lists what each reduce gives:

Reduce Value Error
list (default) the elements in scope order none
set the elements without duplicates none
flat the elements joined: a list element gives its items; without duplicates none
one the single element 0 or 2+ elements
merge a deep attrset two different leaves at one path
elements [ { from; value; } ]: each value with the scope that gave it none
a function [ { from; value; } ] -> { value; errors; } the errors you give

Two aspects give elements to these sinks:

modules/aspects.nix
{ yen, ... }:
{
yen.aspects.base = {
at = yen.kinds.host;
motd = "welcome"; # (1)
ports = 22; # (2)
packages = [
"git"
"curl"
]; # (3) `flat` joins list elements
admin = "alice";
nginx.http.gzip = true;
owners = "ops";
groups = "wheel";
};
yen.aspects.web = {
at = yen.host.web;
motd = "this host serves web";
ports = 22; # (4) a duplicate: `set` keeps one
packages = [
"nginx"
"git"
]; # (5) a duplicate: `flat` keeps one, in order of arrival
nginx.http.servers.www.port = 80; # `merge` joins the attrsets
groups = "audio";
};
}

(1) and (2) give one element each. (3) gives a list element, and flat joins its items. The host web gets the same port and the same packages again: (4) is a duplicate that set drops, and (5) has duplicates that flat drops. Each sink reduces the elements it received:

result
{
admin = "alice";
groups = [
"audio"
"wheel"
];
motd = [
"welcome"
"this host serves web"
];
nginx = {
http = {
gzip = true;
servers = {
www = {
port = 80;
};
};
};
};
owners = [
"host"
];
packages = [
"git"
"curl"
"nginx"
];
ports = [
22
];
}

A failed reduce is an error record. The other sinks still work. This variant breaks one rule on each line:

variants/conflicts.nix
# Each line breaks one sink rule.
{ yen, ... }:
{
yen.aspects.mistakes = {
at = yen.host.web;
admin = "bob"; # (1) `one`: two admins
ports = 70000; # (2) `of`: not a port
nginx.http.gzip = false; # (3) `merge`: two different leaves at one path
};
}

(1) gives a second admin to a one sink. (2) is not a port, so of rejects it. (3) is a leaf that differs from another one at the same path, and merge rejects it. The records:

result
[
{
class = "fixable";
code = "one";
error = "one: got 2 elements";
from = "fleet:main/host:web";
got = 2;
path = [ ];
sink = "admin";
want = 1;
}
{
class = "fixable";
code = "merge";
error = "merge conflict at http.gzip";
from = "fleet:main/host:web";
path = [
"http"
"gzip"
];
sink = "nginx";
}
{
class = "fixable";
code = "of";
error = "sink ports: element of aspect mistakes from fleet:main/host:web fails `of`";
from = "fleet:main/host:web";
path = [ ];
}
]

A reduce is any function from [ { from; value; } ] to { value; errors; }. Write one when the merge order is the point: ranked layers, per-key strategies with provenance, or an export store where a scope can only write its own elements. The engine sets from. An element cannot say where it comes from, so a scope cannot write for another.

ContributeCommunitySponsor