-
-
Notifications
You must be signed in to change notification settings - Fork 14.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
lib/types: Introduce lazyAttrsOf #70138
Conversation
Wow! @arcnmx asked why setting with import <nixpkgs/lib>;
(evalModules {
modules = [
({ foo, bar, ... }: {
config._module.args = {
foo = "Value of foo";
bar = throw "Bar evaluated!";
};
})
];
}).config._module.args.foo Well turns out |
22ddd8a
to
9ee204c
Compare
baab1ff
to
33c443c
Compare
33c443c
to
eb49928
Compare
eb49928
to
be840b1
Compare
Nice, that would replace our https://github.com/hercules-ci/nix-pre-commit-hooks/blob/master/nix/lazyAttrsOf.nix |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mkIf false
I don't think this needs to be part of this PR, but I'd like to share my thoughts.
We could add an 'empty' or 'placeholder' value to the type in cases where mkIf false ...
can be assigned a sane value. It is up to the consuming code to handle this case correctly though.
It requires some care from the module author though, because the 'empty' object must be treated the same as an attribute that doesn't exist.
In my implementation (thanks Domen for linking) I wrote a single generic function that can do both. In hindsight that approach complicated the code unnecessarily. If it's implemented it should probably be a separate type lazyAttrsWithDefault emptyValue t
.
I wrote it that way to make the type to match the capability of attrsOf
as closely as possible but I'm not certain that it's a useful niche to fill.
<para> | ||
An attribute set of where all the values are of | ||
<replaceable>t</replaceable> type. Multiple definitions result in the | ||
joined attribute set. This is the lazy version of <varname>types.attrsOf |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
joined attribute set. This is the lazy version of <varname>types.attrsOf | |
merged attribute set where duplicate attributes are merged according to <replaceable>t</replaceable>. This is a lazy version of <varname>types.attrsOf |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation should point out that the type checking of all attributes still happens.
In my implementation for example it doesn't check the attributes, because my goal was to improve performance.
To replicate that behavior we could add type functions unchecked
and lazyChecked
which wrap a type to disable the check or run it before returning the value. That way you can have attrsOf (lazyChecked package)
to prevent instantiating some packages if you don't need them, but still want to prevent non-package things from being used there.
These functions don't need to block this pr though.
@roberth After thinking about it for a while, what you suggested with a default value sounds like a good idea. Because how this PR is now, there's no way to recover from a let {
nullableAttrsOf = elemType:
/* Implementation behaves like `lazyAttrsOf (nullOr elemType)`
description could be `attribute set of (null for unset or elemType)`
*/;
} This would work really well for NixOS/rfcs#42 too. What do you think of this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the simplicity of "just null it", but it has the problem you mentioned. We can rely on the type parameter to provide the answer.
It removes the need for nullableAttrsOf
, which I find to be non-obvious and potentially ambiguous as to which part can be null: the whole attrset, or the attribute value?
lib/modules.nix
Outdated
if type.check def.value then res | ||
else throw "The option value `${showOption loc}' in `${def.file}' is not of type `${type.description}'.") | ||
(type.merge loc defsFinal) defsFinal | ||
else throw "The option `${showOption loc}' is used but not defined."; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could ask type
for a placeholder value; something like
else throw "The option `${showOption loc}' is used but not defined."; | |
else t.placeholder or (throw "The option `${showOption loc}' is used but not defined."); |
nullOr
can then have { placeholder = null; }
.
Not too complicated I think, while giving anyone the opportunity to add placeholders as appropriate for their use cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not forget to add the synchronization comment from valueDefined
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done that
cd3ed29
to
ddfcad1
Compare
@roberth I updated the code with your suggestion. I've decided to use |
Rebased to fix conflicts |
@roberth Applied your first 2 suggestions |
12ac360
to
e3e6205
Compare
I noticed that I didn't implement type merging, so I did that now. E.g. this now works ( { config, lib, pkgs, ... }: {
imports = [
{
options.foo = lib.mkOption {
type = lib.types.lazyAttrsOf (lib.types.submodule {
options.bar = lib.mkOption {
default = false;
};
});
};
}
{
options.foo = lib.mkOption {
type = lib.types.lazyAttrsOf (lib.types.submodule {
options.baz = lib.mkOption {
default = false;
};
});
};
}
];
config = {
boot.loader.grub.device = "nodev";
fileSystems."/".device = "x";
foo = {
one.bar = true;
two = throw "nothing";
};
};
} Using |
Without this change, accessing `mergedValue` from `mergeDefinitions` in case there are no definitions will throw an error like error: evaluation aborted with the following error message: 'This case should never happen.' This change makes it throw the appropriate error error: The option `foo' is used but not defined. This is fully backwards compatible.
Co-Authored-By: Robert Hensing <roberth@users.noreply.github.com>
The standard attrsOf is strict in its *values*, meaning it's impossible to access only one attribute value without evaluating all others as well. lazyAttrsOf is a version that doesn't have that problem, at the expense of conditional definitions not properly working anymore.
5a95c50
to
9e97e64
Compare
Rebased to fix conflicts, will merge after ofborg is happy |
This pull request has been mentioned on NixOS Discourse. There might be relevant details there: |
Motivation for this change
This introduces
lazyAttrsOf
, which is likeattrsOf
, but lazy in its values. So a single attribute can be accessed without forcing all others.The tradeoff is that it has worse support for conditional definitions, so when defining
foo.some-attr = mkIf false 10
for somelazyAttrsOf
optionfoo
, evaluatingfoo ? some-attr
will betrue
even though it shouldn't be because of themkIf false
. So this option should ideally only be used where conditional definitions aren't needed.I saw this originally in @oxij's #57123, where I thought the justification wasn't very convincing. But since then I found 2 other uses for such a type:
_module.args
be lazy in its values, we can evaluate parts of NixOS configurations without forcing all arguments. Conditional definitions are completely useless for module arguments, so this has no downside. This PR implements this.lazyAttrsOf
values to depend on other values in the samelazyAttrsOf
without having infinite recursion. This is very useful in case absent values are encoded withnullOr
, in which case conditional definitions can be replaced with assigment ofnull
.Best reviewed commit-by-commit.
TODO
cc @oxij @nbp @Profpatsch @rycee