module system: type merging for enum, nullOr and listOf#18380
module system: type merging for enum, nullOr and listOf#18380nbp merged 1 commit intoNixOS:masterfrom
Conversation
|
@ericsagnes, thanks for your PR! By analyzing the annotation information on this pull request, we identified @edolstra, @nbp and @shlevy to be potential reviewers |
lib/modules.nix
Outdated
There was a problem hiding this comment.
@nbp It is needed to merge enums because two enum with different parameters have different names, but I am not very sure of the implications removing this can have.
Is this safe?
There was a problem hiding this comment.
The goal of this check was to prevent the extension of additional option declarations with a type which is not identical. For example, submodule option has a documentation string (name), thus we can extend them with additional submodules. Another example, is with 2 identical types, such as uniq string can be merged with each others.
So this is technically a regression.
The generic question is more subtle than that, which is, is the merge procedure between these 2 types commutative? But honestly, we don't want to answer this one, because in practice this does not hold true, the reason being that the order of the merge is unspecified.
With the addition of typeMerge function, I would recommend to drop the use of these documentation strings (name), in favor of easily comparable types which can be used to uniquely identify a type, and also in a way to carry over the values out-side the comparison set.
So, one potential option would be to replace the inhabitant attribute, by a functor attribute, which can be compared and merged.
{
enum = values: mkOptionType {
functor = { type = enum; wrapped = []; payload = [ values ]; };
};
loaOf = elemType: mkOptionType {
functor = { type = loaOf; wrapped = [ elemType ]; payload = []; };
};
}Thus merging option declarations will recursively check for if the functions are identical, and if we can zip the arguments, otherwise we should throw.
What do you think?
|
@ericsagnes , Thanks. |
|
Even with the comment above which explains why we cannot merge it as-is, this is still some nice work! |
|
Indeed, the functor idea is a great one. Thanks! I slightly adapted your suggestion to something I found more practical to implement:
Other changes:
I tried to play with Maybe I am missing something obvious here? There is always the possibility to add a string representation of the type function name to the functor in case function check is not possible, but I would prefer to have your opinion before going this way. Note: I let the changes in a separate commit for easier review, but will squash when the PR will be ready. |
|
I added a new commit to separate type names and descriptions. I also changed the The check removed in the first commit is now restored in its original state (beside the second commit refactor). |
|
update:
|
lib/options.nix
Outdated
There was a problem hiding this comment.
Hum … Last time I tried that the result was |
|
Thanks for the review! |
58683dc to
d1cccae
Compare
|
update: added some documentation. The commits are kept for review, and will be squashed when the PR is ready. @nbp Could you review the recent changes? |
|
@nbp ping (sorry for spamming but I don't know who could review this PR beside you) |
|
cc @Ericson2314 might be able to provide some feedback |
d1cccae to
373ccdf
Compare
|
Hmm me? I'm not very familiar with the inner workings of the module system. From a general PL perspective, I'd consider making extensibililty opt-in--i.e. have Also, while I'm in general happy to review things note I don't have push access. |
nbp
left a comment
There was a problem hiding this comment.
Sorry for the delay.
I think these changes are going in the right direction. Still, I do not think it is yet acceptable.
lib/types.nix
Outdated
There was a problem hiding this comment.
Shouldn't this just return null, as defined as part of the typeMerge property? Also returning null will display the generic error message about non merge-able options.
Merging 2 identical simple type should be accepted. bool and bool should merge fine without returning null. I suggest you change the default value of typeMerge to defaultTypeMerge functor.
lib/types.nix
Outdated
There was a problem hiding this comment.
We should also guard that f'.wrapped is not null and return null. Otherwise this function would fail with an error message which would mention that we expected wrapped to be an attribute set, while we got null, which is not as helpful as the error message related to the fact that we cannot merge both types.
Do the same for the payload attribute below.
lib/types.nix
Outdated
There was a problem hiding this comment.
nit: The comment should distinguish the two, by mentioning that the description is defined recursively, by embedding the type of the wrapped type, if any.
lib/types.nix
Outdated
There was a problem hiding this comment.
Testing the name in the either type should no longer be necessary iff this is being handler by the defaultTypeMerge function. In which case, only mt1 == null || mt2 == null would be necessary.
lib/types.nix
Outdated
There was a problem hiding this comment.
nit: Should we avoid duplicates in the enumerated values?
lib/types.nix
Outdated
There was a problem hiding this comment.
Why should these be re-computed, as we already know them as t1 and t2 from the either function arguments?
lib/types.nix
Outdated
There was a problem hiding this comment.
We should check that f.type.name == name before interpreting f'.wrapped as being a list.
lib/types.nix
Outdated
There was a problem hiding this comment.
This is not a commutative behaviour, either both types should be mergeable, or we should not be able to merge this either type.
|
Thanks for the review! I addressed all the points in the latest commit, it looks cleaner now.
There was a problem with that as I chose to add the The commit also introduce a few other changes:
|
nbp
left a comment
There was a problem hiding this comment.
Apart from the submodule type typeMerge function, which would fail on NixOS, this patch looks perfect.
This feature would be awesome once we could use it for all the examples listed in the documentation nit below.
lib/modules.nix
Outdated
lib/modules.nix
Outdated
There was a problem hiding this comment.
nit: add parenthesis around this expression. (bothHave "type" && ...)
lib/types.nix
Outdated
lib/types.nix
Outdated
There was a problem hiding this comment.
note: This assumes that null is not a valid payload, but I am fine with this restriction.
lib/types.nix
Outdated
There was a problem hiding this comment.
sep does not have any meaning as part of a functor. I think what you could do is:
{
functor = (defaultFunctor name) // {
payload = sep;
binOp = sepLhs: sepRhs:
if sepLhs == sepRhs then sepLhs
else null;
};
}
lib/types.nix
Outdated
There was a problem hiding this comment.
With the new functor from above, the defaultTypeMerge should be enough. So removing this code should be fine.
There was a problem hiding this comment.
nit: we cannot remove, unless we do not include a module.
There was a problem hiding this comment.
foo, bar, and baz might not be easy to follow, maybe taking a concrete example might help get a better understanding of the feature provided by extensible option types.
For examples:
- service: audio; backends: alsa, pulseaudio.
- service: webserver; backends: apache, nginx.
- service: init-system; backends: upstart, systemd, initd.
- service: display manager; backends: kdm, slim.
There was a problem hiding this comment.
nit: I would replace Extended by Extensible, and do the same for all the references below as well.
Extended would apply to the result, not the property of a type.
lib/types.nix
Outdated
There was a problem hiding this comment.
I highly doubt this would work with the current NixOS modules, as we at least use that for the fileSystems.
Currently the merging process of submodules is handled by mergeOptionDecls, but we still expects to be able to merge the type. The opts value is similar to the list of enumerated values of the enum type, but as the submodules are extracted, I guess you could just define the following functor:
{
functor = (defaultFunctor name) // {
# Merging of submodules is done as part of mergeOptionDecls, as we have to annotate
# each submodule with its location.
payload = [];
binOp = lhs: rhs: [];
};
}9424bd9 to
c34a62f
Compare
|
Thanks again for the review! Addressed all the points in the last commit and added a changelog. |
nbp
left a comment
There was a problem hiding this comment.
Apart from the documentation nits, the technical part is good and mergeable.
There was a problem hiding this comment.
nit: namely enum and submodules and any composed forms of them.
There was a problem hiding this comment.
nit: separatedString and submodules are also value types.
There was a problem hiding this comment.
nit: to extend certain types, such as enum, through multiple option declarations of the same option across multiple modules.
c34a62f to
10bc213
Compare
|
Thanks! Updated the documentation and squashed the commits so this should be ready to merge. |
|
@ericsagnes Awesome work! Thanks for your dedication and patience :) |
|
Thanks for merging! |
Motivation for this change
This adds type merging for
enum,nullOrandlistOftypes.It allow to extend type definitions for
enum,nullOr enumandlistOf enumoptions over multiple modules files in a decentralized manner.Idea come from #15096 (comment).
Example use case: Input methods
This functionality can be used extend sum types defining a choice without changing the base module.
There can be only one input method enabled system-wide, so the enabled input method option is declared as an
enumin the central input method module (default.nix).Every input method has its own set of options and config and so is managed in its own module file.
Currently, options declarations are closed so the declaration in
default.nixmust contains all the possible choices:default.nix
That means that any addition or removal of input methods result in a change in the
enableddeclaration indefault.nix.With type merging, it is possible to make
enableda placeholder in the main module:default.nix:
And to extend it in other input methods modules files:
ibus.nix:
As a result, option declaration is decentralized and modularity improved.
Manual
In the manual, all the files that extends or declare the type are listed in
Declared by:, sample output:This functionality is useful for any case where modular sum types options could make sense (display managers, window managers, desktop managers modules, ...)
cc @nbp
Things done
(nix.useSandbox on NixOS,
or option
build-use-sandboxinnix.confon non-NixOS)
nix-shell -p nox --run "nox-review wip"./result/bin/)