lib/types: add record (simplified submodule)#334680
lib/types: add record (simplified submodule)#334680MattSturgeon wants to merge 14 commits intoNixOS:masterfrom
record (simplified submodule)#334680Conversation
42d9248 to
746b372
Compare
746b372 to
654bd51
Compare
|
can we nest records? something like this? |
Yes. Currently nesting records (or submodules) within a record is the only way to achieve nested options. So you can't do (for example): settings = record {
fields {
foo.bar = mkOption {};
};
}But you can do: settings = record {
fields {
foo = mkOption {
type = record {
fields = {
bar = mkOption {};
};
};
};
};
}This limitation was a design decision by @roberth in the original PR #257511, for performance reasons. I suppose it could be re-evaluated in the future though.
Almost. All fields are options, not types. |
|
thanks for the answer i think something like this is an important but missing feature currently
of course, i was just simplifying |
|
@MattSturgeon any updates on this? i find myself less motivated to move forward in a few areas because the solution this PR offers to RFC42 provides a much better way than what currently exists @roberth and @infinisil - is it agreed that this PR is the proper way to address optional fields, or are either of you still not entirely convinced and reviewing other solutions? |
|
I'm in favor. Besides my earlier reasons, we've seen that taggedSubmodule is rather hard to implement in a complete way, whereas a solution based on Probably it can even reuse Anyway, I'd like to hear @infinisil's thoughts too. |
roberth
left a comment
There was a problem hiding this comment.
Thoughts and possible suggestions (if they're good; this is all very new)
lib/types/record.nix
Outdated
There was a problem hiding this comment.
Maybe I was wrong to use mkOption.
It means that we have these extra checkFields checks to cover known problems, but it also couples record and module, holding back both types.
It seems that all of the reuse we do get is from option types, not options themselves, or we wouldn't be peeling out type and default ourselves, below.
And we don't even want defaults in the optional fields.
A mkField would be simpler, more efficient, and independent of modules. It seems that we only need type and default, but internal/visible and meta could be added.
There was a problem hiding this comment.
I agree a mkField would make sense to further de-couple records from modules. This would also allow fields to declare whether they are optional, rather than having a separate optionalFields argument.
There was a problem hiding this comment.
I added lib.fields (lib/fields.nix) to host a mkField function, although maybe it'd be better to just have mkField in lib/options.nix?
lib/types/record.nix
Outdated
There was a problem hiding this comment.
| # TODO: should we prevent fields from having a wildcard? | |
| # TODO: should we prevent `fields` from having a `_wildcard`? |
Seems easy and cheap enough, especially if we assert it here in the nestedTypes value, which I believe is only accessed when doing type merging, which is somewhat rare. (I'd have to check)
There was a problem hiding this comment.
The current nestedTypes is not returning the fields anymore, so this is no longer relevant.
Not sure on that design decision... If we do want to expose fields via nestedTypes, it could be as a nestedTypes.fields set.
lib/types/record.nix
Outdated
There was a problem hiding this comment.
This is a weird one. It could work like the type emptyValue, and work like lazyAttrsOf here - a trade-off between omitting the attribute entirely, as one may expect, or avoiding infinite recursions, as one may expect.
attrsOf behavior: omit attribute when value is mkIf false. Infinite recursion for defs like settings = mkIf cfg.settings.foo { bar = "bar"; }
lazyAttrsOf behavior: keep the attribute even if mkIf false, but insert an empty value, or throw.
There was a problem hiding this comment.
It sounds like we need a lazy and non-lazy mode for record types, since its users may wish for either behaviour.
In nixvim, for instance, we probably want the non-lazy behaviour where optional fields are not present in the resulting attrs.
This is because we usually want to be able to recursively map the attrs into a lua table using our version of lib.generators.toLua.
I guess we could have our toLua generator wrap recursion in tryEval and not render anything that throws, but that seems like an overly large hammer for this issue.
67f9cc8 to
3a0417a
Compare
|
i keep seeing (and writing) more nix code that would benefit from this so much - these cc @artemist because of how much they could benefit from this |
|
I guess then i'm also interested in this PR. Came over from: #377718 I'll check if i find some time helping out. |
Co-authored-by: Robert Hensing <roberth@users.noreply.github.com>
f9da387 to
5af8e09
Compare
|
If course! Any help is appreciated. Are you at NixCon? I've let this sit long enough that I'll need to sit down and re-read it before continuing. IIRC the biggest blocker was finding inspiration to write new test cases. If you're motivated to work on this, you're welcome to take over. Or with clear/frequent feedback I'll be more motivated to work on this. |
I'm around, mostly in building 4 |
|
After looking into this for a while the biggest foreseeable todos are
|
We chatted a bit about this at NixCon. We already have multiple representations for things that "are" options in one way or another. Allowing fields to be represented in the documentation-flavored option representations sounds more than ok to me. Ultimately the UX is pretty much the same, whether it was originally a field or |
|
Fiy i also tried adding |
|
thanks @hsjobeki - i really appreciate you tackling this! 🙇♂️ |
This can be a trap sometimes. Best to make sure that it's possible in plain Nix first. tl;dr I think we need to drop the I think you're referring to option declarations. Which go a bit beyond what plain Nix operations do, except making sure the
nix-repl> with lib; (lib.evalModules { modules = [ { config._module.check = false; options.foo = mkOption { default = 1; }; options.bar = throw "nope"; } ]; }).config.foo
1Records could be designed to also allow this, without having to opt in or compromising name checking, but for that we need the required fields and optional fields to be fed in as distinct arguments. record {
requiredFields = <...>;
optionalFields = <...>;
}That's probably the only way to assess the validity of a record's set of attribute names without having to evaluate any Conversely, if we only have a |
|
@roberth since we need to move But this also requires handling of Those are handled by Or we could explicitly exclude the apply and readonly features? I would expect at least readOnly to remain supported? |
|
Short note for this PR: Add tests for recursive records. Key difference: submodules dont have optional options. records can be fully optional. -> This opens them up for defining (infinite) recursive types. That describe both finite and infinite values. Usually recursive types have some kind of terminator, i.e. treeType = record {
optional.child = treeType;
required.payload = types.str;
};
# --- example finite value ---
treeValue = {
child = { payload = "child"; } # absence of a further child
payload = "parent";
}
# --- example infinite value ---
treeValue = {
child = { payload = "child"; child = treeValue; } # self reference
payload = "parent";
}Q: how far do we need to support these recursive types? I.e. through Similar/related to: #422272 |
I think It's already possible to have infinitely recursive submodule-option trees; I've done this myself a few times where I have tree structures, e.g.: Additionally, most
The original implementation did that and just had some assertions that unsupported features were not used: if value._type or null != "option" then
throw "Record field `${lib.escapeNixIdentifier name}` must be declared with `mkOption`."
else if value?apply then
throw "In field ${lib.escapeNixIdentifier name} records do not support options with `apply`"
else if value?readOnly then
throw "In field ${lib.escapeNixIdentifier name} records do not support options with `readOnly`" |
Description of changes
Work in progress draft, based on #257511 by @roberth, but without
types.fixand with additional support for optional fields.Pushing an early draft, I'll polish up the changes and this description shortly.
The main TODO for me is to add additional test-cases to cover more exotic useage. I've added a few inline TODO comments regarding things I'm unsure of or plan to investigate.
More TODO from https://github.com/NixOS/nixpkgs/pull/257511/files#r1621120811:
config-driven submodules? #158598definedOptionsOnlyoption toevalModules#333799types.record,types.fix#257511configdespite option #158594cc @roberth @infinisil @GaetanLepage @traxys
Things done
nix.conf? (See Nix manual)sandbox = relaxedsandbox = truenix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD". Note: all changes have to be committed, also see nixpkgs-review usage./result/bin/)Add a 👍 reaction to pull requests you find important.