-
Notifications
You must be signed in to change notification settings - Fork 0
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
What about modules to define modules? #1
Comments
Oh thanks for reminding me of this amazing talk which likely had originally inspired me thinking about all this.
Hm, sounds interesting. Would you mind providing an example of how you imagine this to look like? |
I think Nix community didn't watch it enough, but it ends towards Nickel like solution, while we already have tools to make it nice.
First, I'm not expert in modules, but I realize that we divide it in two, where it could be divided in three. Normal user: Thinking in modules as 1 file (config) config values (declarative) The nearest I have is this, but would be good if I haven't to call any function and what inside let to inside a |
What if instead of this: { lib, ...}:
let
Node = lib.types.submodule {
options.tags = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = ["logstash"];
default = [];
description = "Define the tags of this node in the project cluster";
};
};
env = lib.mkOption {
type = lib.types.attrsOf Node;
default = {};
example = { logstash01 = { tags = ["logstash"]; }; };
description = "nodes of this environment";
};
Project = lib.types.submodule {
options.git = lib.mkOption {
default = "";
type = lib.types.str;
example = "https://github.com/DavHau";
description = "git project url";
};
options.desc = lib.mkOption {
default = "";
type = lib.types.str;
example = "Elastic stack config files";
description = "Description of the project";
};
options.docs = lib.mkOption {
default = "";
type = lib.types.str;
example = "https://google.com";
description = "url of main documentations";
};
options.dev = env;
options.prd = env;
};
in
{
options.project = lib.mkOption {
type = lib.types.attrsOf Project;
default = {};
example = { elk.desc = "elastic stack"; elk.dev.logstash01.tags = ["logstash"]; };
description = "Information about our git projects";
};
} We do this: {lib, ...}:
let
env.default = {};
env.example = { logstash01 = { tags = ["logstash"]; }; };
env.description = "nodes of this environment";
env."<name>".options.tags."*".type = lib.types.str;
env."<name>".options.tags.example = ["logstash"];
env."<name>".options.tags.default = [];
env."<name>".options.tags.description = "Define the tags of this node in the project cluster";
in
{
options.project.default = {};
options.project.example = { elk.desc = "elastic stack"; elk.dev.logstash01.tags = ["logstash"]; };
options.project.description = "Information about our git projects";
options.project."<name>".options.git.default = "";
options.project."<name>".options.git.type = lib.types.str;
options.project."<name>".options.git.example = "https://github.com/DavHau/pkgs-modules";
options.project."<name>".options.git.description = "git project url";
options.project."<name>".options.desc.default = "";
options.project."<name>".options.desc.type = lib.types.str;
options.project."<name>".options.desc.example = "Elastic stack config files";
options.project."<name>".options.desc.description = "Description of the project";
options.project."<name>".options.docs.default = "";
options.project."<name>".options.docs.type = lib.types.str;
options.project."<name>".options.docs.example = "https://google.com";
options.project."<name>".options.docs.description = "url of main documentations";
options.project."<name>".options.dev = env;
options.project."<name>".options.prd = env;
} Then we recurse over this as: |
At first glance this looks pretty cool. What do you think about this style proposed in the previous comment @roberth ? |
I think the module system already has too much syntax sugar. Every time you add syntax sugar, you introduce at least one corner case. In your example, each
I find this quite unpleasant to read. Nesting the attrset syntax might improve it for me, but at that point you've only removed the need for Instead of adding more sugar, I'd prefer a simplification, such as turning the options into an "attrsOf option" rather than a tree. This also removes the need for |
It's rare for the options not to come with default values. I don't find a separation into files useful, but I guess it could serve as a mental model. I wouldn't call them files though, but something more abstract in that case.
I don't distinguish between implementation and config. |
Good old joke The usage of an existing module is nice and easy, but the creation of a module is a fine art that requires a lot of fu. Part of Eelco video about creation of an module 'lang' is that the type definition isn't easy, and maybe is why we have Nickel in progress that may fix the issue, but isn't declarative/homoiconic as nix module. And we may have some of extraOptions because the pains of type definition. Like "i would like to properly type my options, but nah.. too much work. just add extraOptions". That is not to say that my suggestions are good, but simplifying type definition is (even with other methods).
This is a function call, this project suggest using module system instead of mkDerivation directly because is declarative instead of imperative and that impacts readability and modularity. To be honest,
Mixing this quote, previous one and the next. Last week I discovered. tweag/nix-hour#15 that surprised me so much, that I tried to actually implement it in different files, extending my previous example (as I told this is a real example). { lib, ...}:
let
SSH = lib.types.submodule {
options.host = lib.mkOption {
default = "";
type = lib.types.str;
example = "127.0.0.1";
description = "SSH host";
};
options.user = lib.mkOption {
default = "";
type = lib.types.str;
example = "batatinha";
description = "SSH user";
};
};
ssh = lib.mkOption {
default = {};
example = { host = "127.0.0.1"; user = "batatinha"; };
description = "ssh options";
type = SSH;
};
Node = lib.types.submodule { options.ssh = ssh; };
env = lib.mkOption { type = lib.types.attrsOf Node; };
Project = lib.types.submodule {
options.dev = env;
options.prd = env;
};
in {
options.project = lib.mkOption {
type = lib.types.attrsOf Project;
}; but could be let
env."<name>".options.ssh = {
default = {};
example = { host = "127.0.0.1"; user = "batatinha"; };
description = "ssh options";
options = {
user.default = "";
user.type = lib.types.str;
user.example = "batatinha";
user.description = "SSH user";
host.default = "";
host.type = lib.types.str;
host.example = "127.0.0.1";
host.description = "SSH host";
};
};
in {
options.project."<name>".options.dev = env;
options.project."<name>".options.prd = env;
}
Thanks, your feedback is appreciated, I have a very strange taste, you may see it by my indentation and repetition. Maybe my editor makes easier to copy lines than indent lines. Or because of my java properties background, I think modules like java properties are easy to any OPs reason about than our
As most of current config is some sort of 'json', I think 'attrsOf =
I suggested those (
It would only makes sense as default behavior after a long term as lib and if gains popularity. So yes, name collisions but as opt in.
The best example for that is: cruel-intentions/devshell-files#8 (devshell files without devshell, because we have devenv, and others) and cachix/devenv#75 (make services usable outside devenv), like make service definition usable in other init systems, describing both options and how the modules converts it to systemd services files, would be more reusable if options are in one file without any 'config' (except defaults). But in this case is more because while we can declarative describe options (interface), I can't say the same of how we convert that to final format/"config"/file/derivation (implementation). That, sadly, would not make easier to convert expected type (interface) to final form (data wrangling). My current implementation isn't working (yet), but I think is good to have here for curiosity. { lib, ...}: opt-def:
let
attrs = attrSet: builtins.concatStringsSep ", " (builtins.attrNames attrSet);
split = path: opt-def':
let prop =
if builtins.hasAttr "type" opt-def' then "type"
else
if builtins.hasAttr "_type" opt-def' then "_type"
else
if builtins.hasAttr "options" opt-def' then "options"
else
if builtins.hasAttr "<name>" opt-def' then "<name>"
else
if builtins.hasAttr "*" opt-def' then "*"
else
builtins.throw ''
Option type def error, we don't known what to do with this
Expected ${path} with one of these attr "type", "options", "<name>", "*" but got
${attrs opt-def'}
'';
in {
inherit prop;
type = opt-def'."${prop}" or null;
mod = builtins.removeAttrs opt-def' [prop];
};
toSub = path: opt-def': { options = builtins.mapAttrs (prop: sub: lib.mkOption (toOpt "${path}.${prop}" sub)) opt-def'; };
toOpt = path: opt-def':
let
splited = split path opt-def';
ifHas._type = { prop, type, mod }: opt-def';
ifHas.type = { prop, type, mod }: mod // { type = type; };
ifHas."<name>" = { prop, type, mod }: mod // { type = lib.types.attrsOf (toOpt "${path}.${prop}" type); };
ifHas."*" = { prop, type, mod }: mod // { type = lib.types.listOf (toOpt "${path}.${prop}" type); };
ifHas.options = { prop, type, mod }: mod // { type = lib.types.submodule (toSub "${path}.${prop}" type); };
impl = ifHas."${splited.prop}";
in builtins.trace "${path} ${splited.prop} (${attrs (impl splited)})" impl splited;
in toSub "options" opt-def
# should be called like
# let
# sadnessAndSorrow = import ./im_bad_at_naming_things.nix { inherit lib; };
# # isn't working, is an appropriate soundtrack
# in {
# imports = [ (sadnessAndSorrow ./projects.nix) (sadnessAndSorrow ./ssh.nix) ];
# } |
Maybe 1% better: { lib, ... }:
lib.simpleModule {
options = .....;
config = .....;
} Still not sure if it's a good idea, because people will try to use this for as long as possible, even if they really shouldn't because they need all sorts of workarounds. Aside from having to learn more unnecessary syntax. |
Now we have a POC. I tried solve collision problem by adding parameters to let user define the keywords. I tried validation of attributes but module system, give some strange errors when IE. someone type |
I think people don't declare modules enough.
Maybe because declare a module (options) isn't fun¹.
What about modules to define modules?
¹ https://www.youtube.com/watch?v=dTd499Y31ig
The text was updated successfully, but these errors were encountered: