Skip to content

The cycle form

Each key of an aspect gives one element. For 0 to n elements, or to ask the tree for data, give the aspect a cycle. As in Cycle.js, a cycle is a function from sources to sinks. This one gives one bridge for each guest of a host. The kinds host and guest are in the example code.

modules/network.nix
# A cycle: it reads sources (the tree, the sinks) and writes sinks. It gives 0 to n elements.
{ yen, ... }:
{
yen.aspects.vm-network = {
at = yen.lib.below yen.kinds.guest; # every host with a guest
cycle = # (1)
{ tree, ... }:
let
vms.children = yen.kinds.guest; # (2) a request: the guest children
in
{
tree = [ vms ]; # (3)
bridges = map (g: "br-${g.name}") (tree.select vms); # (4) one element per guest
errors = [ ];
};
};
}

(1) is the cycle. Its sources are tree, scope and one source for each sink. It returns sinks by name, and tree requests. (2) is a request: the children of this scope that are guests. (3) sends the request to the tree. (4) reads the answer, with tree.select, and gives one element for each guest. The host has two guests, so the cycle gives two bridges:

result
{
server = [
"br-ci"
"br-db"
];
}

A request is one question. tree.select request gives its one answer, or null when nothing answers it or the cycle did not ask it. The request is its own name, so it has no string to mistype. Bind it in a let and use it twice, as vms above. These are the requests:

Request Answer
nearest = yen.kinds.environment the nearest scope of that kind, or null
parent = true the parent scope, or null at a root
children = predicate the children where the predicate holds
scopesOf = instance every scope of an instance
holds = predicate true or false at this scope
instances = yen.kinds.host every instance of that kind, with its data
graph = true the whole fleet: instances with data and the structure queries, for readers of everything like docs and diagrams

An element reads its args: its scope, the kinds it asks for, and the values its sinks give. To read another instance, it asks the tree. It never reads that instance’s fields directly. yen knows what every element reads, so a change is refused only where it reaches. Here a host asks for every host and counts the other ones:

modules/reads.nix
# A host asks the tree for every host, and counts the other ones.
{ yen, ... }:
{
yen.aspects.peers = {
at = yen.kinds.host;
cycle = # (1)
{ tree, scope, ... }:
let
hosts.instances = yen.kinds.host; # (2)
in
{
tree = [ hosts ]; # (3)
out = [
{
peers.${scope.name} = builtins.length (
builtins.filter (h: h.name != scope.name) (tree.select hosts) # (4)
);
}
];
};
};
}

(1) is the cycle. (2) is a request for every host. (3) sends it to the tree, and (4) reads the answer. The fleet has one host, so it has no peers:

result
{
server = 0;
}

An element that reads a field of another instance is refused. This variant does that at (1):

variants/read-addr.nix
# An element reads a field of another instance: refused, with the fix.
{ yen, ... }:
{
yen.kinds.host.fields.addr = yen.lib.is builtins.isString;
yen.host.server.addr = "10.0.0.1";
yen.aspects.read-addr = {
at = yen.kinds.host;
cycle =
{ ... }:
{
out = [ { addr.server = yen.host.server.addr; } ]; # (1)
};
};
}

The error names the fix: “yen.host.server.addr: an aspect reads instance data only through its args: take host as an arg, or follow a ref field of an arg”.

ContributeCommunitySponsor