Skip to content

Nested kinds

A kind can declare kinds inside it. A nested kind has no name of its own at the top: its instances live under one instance of the parent. A user of a host is a good example.

You write the nested kind in the key kinds of the parent:

modules/host.nix
# A kind declared inside a kind. `user` exists only under `host`: there is no `yen.kinds.user`.
{ yen, lib, ... }:
{
yen.kinds.host = {
fields.addr = yen.lib.str;
kinds.user.fields.shell = yen.lib.str; # (1)
};
}

(1) declares the kind user inside host. There is no yen.kinds.user: the path is yen.kinds.host.kinds.user.

You write the instances of a nested kind inside the instance of the parent:

modules/hosts.nix
# The users of a host are written inside that host. Two hosts can have a user with the same name.
{ yen, ... }:
{
yen.host.laptop = {
addr = "10.0.0.1";
user.vic.shell = "fish"; # (1)
user.ann.shell = "bash";
};
yen.host.server = {
addr = "10.0.0.2";
user.vic.shell = "zsh"; # (2)
};
}

(1) is the user vic of the host laptop: yen.host.laptop.user.vic. (2) is the user vic of the host server. Two hosts can each have a user vic: they are two instances.

A ref of a nested kind has the same path as its declaration. The aspect below gives one line per user:

modules/report.nix
# The ref of a nested kind is the path of its declaration: `yen.kinds.host.kinds.user`.
{ yen, ... }:
{
yen.aspects.report = {
at = yen.kinds.host.kinds.user; # (1)
report = { scope, ... }: "${scope.id} ${scope.shell}"; # (2)
};
}
result
[
"fleet:main/host:laptop/user:ann bash"
"fleet:main/host:laptop/user:vic fish"
"fleet:main/host:server/user:vic zsh"
]

(1) is the ref of the nested kind. (2) reads the current scope with the arg scope. The id of a scope shows the parent: the scope of a user is a child of the scope of its host.

The arg { user, ... } also works: a function arg may name a nested kind by its leaf name. yen resolves the name up the ancestors only, never a sibling. The root kind that collects report is in the example code.

A kind spawns its nested kinds by default, from the key with the same name. Here host spawns the users that you write in it. You do not write spawns.user.

An explicit spawns.user replaces the default. This one keeps only vic:

variants/explicit-spawns.nix
# An explicit `spawns.user` replaces the default. This host spawns only the user `vic`.
{ yen, ... }:
{
yen.kinds.host.spawns.user = s: { inherit (s.user) vic; };
}
result
[
"fleet:main/host:laptop/user:vic fish"
"fleet:main/host:server/user:vic zsh"
]

Use a nested kind for children that exist under one parent. A kind that nothing spawns, such as a type for data, stays a flat kind at the top.

The check of this page evaluates the same fleet with flat kinds. There user is a top kind, and host spawns it by hand. The scopes and the report are equal. Only the nested form keeps user out of the top names:

flat/host.nix
# The same fleet with flat kinds: `user` is a top kind and host spawns it by hand.
{ yen, lib, ... }:
{
yen.kinds.host = {
fields.addr = yen.lib.str;
fields.user = yen.lib.attrs;
spawns.user = s: s.user;
};
yen.kinds.user.fields.shell = yen.lib.str;
}

A field of a kind cannot have the name of a nested kind, because both use the same path in an instance:

variants/reserved.nix
# A field named like a nested kind shares its path in the instance.
{ yen, lib, ... }:
{
yen.kinds.host.fields.user = yen.lib.str;
}
result
[
{
class = "fixable";
code = "reserved";
error = "nested kind `user` of kind host: the kind has a field of that name";
fix = "rename the nested kind";
from = "declarations";
path = [
"yen"
"kinds"
"host"
"kinds"
"user"
];
}
]

A key that a nested kind does not have is an unknown key, with the path of the nested kind:

variants/not-a-kind.nix
# `yen.kinds.host.kinds.user.feilds` is not a key of a kind.
{ yen, ... }:
{
yen.kinds.host.kinds.user.feilds.shell = "x";
}
result
[
{
class = "fixable";
code = "unknown-key";
error = "`feilds` is not a key yen reads here";
fix = "use one of the keys in `want`; yen modules are not NixOS modules";
from = "variants/not-a-kind.nix";
near = [
"fields"
];
path = [
"yen"
"kinds"
"host"
"kinds"
"user"
"feilds"
];
want = [
"fields"
"spawns"
"sinks"
"kinds"
"aspects"
];
}
]

The instances of a nested kind are a set of names. A value that is not a set fails its type:

variants/bad-group.nix
# The users of a host are an attrset of instances. A string is not.
{ yen, ... }:
{
yen.host.desk = {
addr = "10.0.0.3";
user = "vic";
};
}
result
[
{
class = "fixable";
code = "check";
error = "user is not an attrset";
fix = "write user.<name> = { ... }";
from = "host:desk";
got = "string";
path = [
"host"
"desk"
"user"
];
want = "attrset";
}
]
ContributeCommunitySponsor