Dendritic-unflake: Example Dendritic setup without flakes nor flake-parts

An example repository showing a Dendritic nixosConfiguration without flakes nor flake-parts see context.

This example uses unflake to manage dependencies and provide inputs just like those on flakes.

10 Likes

What benefit do you see with this pattern (using deferredModule)?

Not sure I understand the question, it’s always been about deferredModules. The repository only serves as an example of using the same (dendritic) organization pattern without flake-parts. Its purpose is showing that the pattern does not depend on flake-parts lib (even if the pattern was discovered/documented in that setting), as long as we have a similar modules option.

I’m not comparing it to flake-parts, I’m comparing it to regular modules - what benefits do you see for the modules.* pattern compared to writing a ā€œnormalā€ config?

I tend to write a lot, so I hope not to be repeating myself or others. The original Dendritic discourse post (linked in opening post) has links to resources and people talking about the benefits.

Dendritic uses regular nix modules, because at the end, it is just about merging modules as shown by the example of this post (which uses evalModules). If by ā€œregularā€/ā€œnormalā€ you mean, the way we see nix configuration examples in the wild, I’d say:

  • Most examples look monolithic. This has always been funny to me, many many nix examples I have seen since I started using nix, frequently show a huge flake.nix file, or at least assume you are editing a huge flake.nix. And it is like if nodejs people suggested to use inlined huge js program in the package.json file itself. IMHO flake.nix should be used as an inputs manifest and the outputs function should import as soon as possible. In the dendritic pattern, you tend to use flake.nix as just a manifest, because it is only used to define inputs and immediatly importing all of ./modules files. This is the reason why the non-flakes example has exactly the same structure as a flakes-enabled dendritic setup: in the non-flake version the inputs manifest is just not flake.nix (just like you can use different js package managers, you can use flakes, npins, or unflake and your configuration remains the same as shown by our non-flakes example)

  • Semantic meaning. Nix files just contain a single nix expression. A file could be a boolean, an string, an array or any other nix expression. Because of this, importing a nix file requires you to know what kind of expression it contains. is it a function expected to be called? is it an array of strings representing systems?. Is the meaning of that function a package ? or is it a nixos module ?. In dendritic, all files have the same semantic meaning. They are all a nix module. (either an attribute-set or a function to attribute-set). Because of this, we can merge all files (read more about this on my article on the first link).

So you don’t have to worry about what a file means, they are all modules, internally each module can define packages, apps, or can define nixos or home configuration sub-modules. but the top-level expression is always a module.

This is where flake-parts entered the scene. A flake-parts module allows you to define perSystem packages, shells, or any amount of flake.modules.<class> internally. And I believe this is why the pattern was discovered on flake-parts. Each file (top-level module) adds something to the flake (or your configuration to avoid limiting ourselves to flakes)

For me, the main benefit is flipping-the-configuration-matrix (second link on this reply): that any file can enhance any modules.* configuration. People can organize files semantically by what each of these files contributes to the system (or parts of the system), not by where features are applied.

Each of these top-level modules (either flake-parts module or non-flake-parts modules like the ones shown in the example) can contribute to several configuration sub-modules modules.* (to enhance diverse nix configuration classes), contributing to OS-level, home-level, editor-level configurations.

These partial (deferred) modules get at the end merged by the nix module system itself from the many modules.* definitions they have, and the additive result of that is used as a single module in nixos/home configurations. The OP example is trivial, as it defines all modules in the same file but the real advantage is when many files contribute to the same aspect.

Of course we have many options/frameworks/libs to organize nix configurations, this is a good thing because the most important thing about a configuration-organization is that it suits well your mental model.

Many frameworks impose a defined structure, kinda what rails did originally, to aid people with organization. Most of the time, these organizations are defined by the meaning of the expression contained in files. You see for example directories for packages, other directory for nixos-configurations, others for home-configurations, others for nixvim-configurations. The Dendritic proposal is not to do that, do not impose any naming restriction and do not impose what nix-configuration-classes the framework supports. Each dendritic module can configure any nix configuration classes.

In the end, it is about merging (the job of the evalModules system) many tiny deferred modules contributed by many files into a single final nix/home configuration.

So, the benefit is more how you model things in your mind, how you organize and name things instead of purely technical (how stuff is built).

hope that helps.

4 Likes

UPDATE: The example repo now showcases 6 different branches, each showing a different kind of top-level module, or dependency locking.

I’m still a bit mystified; I also question why it’s valuable to have a meta-module layer with an unstructured modules option instead of using the base-layer modules directly.

I’ve read most of the linked posts, and it seems like the point of the dendritic pattern is two instances of decoupling:

  • Decouple features from their consumers (instead of large configuration of hosts, define hosts as aggregates of features and have smaller configuration of features) — this is obviously good.
  • Decouple the files that define features from the names used to import them — this is not obviously good, and in fact I think it’s harmful. Using the meta-module pattern, if a host depends on a feature modules.nixos.trendy-stuff and I want to know what that entails, I have to search the entire repository for anything that defines something under modules.nixos.trendy-stuff. Without the meta-module pattern, a host depends on a file ../trendy-stuff (assuming a file naming convention of nixos/hosts/my-laptop.nix, following my two-deep advice at the bottom of this post), and I know exactly where that is.

Can the example be elaborated in a way that demonstrates why you’d actually want to have this second decoupling? As it is, it’s fairly trivial to translate it out of the dendritic idiom by replacing dots with slashes, moving not-meta-level modules to their own files (nixos/statics/default.nix instead of nixos.statics = ...;, etc.), and then getting rid of the self special arg and the import-tree dependency, neither of which are load-bearing after module imports are made explicit. Everything is still a module and hosts are still decoupled from the configuration of features that they use, with no extra verbosity (because import relationships in your example are already explicit, only based on attrpaths instead of file paths; we’re just replacing dots with slashes). Seems like a clear win to me. What’s the trade-off?

3 Likes

the example from this post is focused on exploring if it was possible to have non-flakes / non-flake-parts dendritic setups (as the title says) – given that the pattern was discovered in flakes+flake-parts. I wanted to know if it was possible and also to serve as evidence in favor of dendritic#15 to be merged. so I guess this particular repository is not suited for exploring file-organization/location-of-definitions, because that is not the purpose of it.

I believe the Dendritic pattern does not mandate automatic imports, it enables it (most of us using it, do automatic-imports because all files have same kind of content, and renaming, moving files around has no impact and this aids a lot on refactoring and evolving a large infra), hope some people like @drupol, @quasigod or others can testify how we have refactored our respective infras over different dendritic-compatible frameworks (far beyond flake-parts) and it all still works.

Yes, I understand the argument. Any given person can create spaghetti code if you give them enough time or AI tokens, but how do you prevent that is more a matter of taste, experience and good architecture design. Same happens here, even with ā€œnormalā€ non-dendritic setups, even with manual imports, you could have nixos-module nix-files contributing to environment.systemPackages all over the place or just in a single place. Same happens here, but with modules, no mergable-nixos-option has a restriction that it must be defined at a single place. Merging options is the purpose of the module system, and merging only makes sense if you have more than one definition, where those definitions are located depends on you.

2 Likes

That’s what we’re discussing, right? Whether the dendritic pattern — specifically, the second decoupling of files from import names — is compatible with good architecture design?

Yes, exactly, that’s what the module system enables. NixOS options are outputs and it often makes sense to merge multiple definitions of them because a given output could be influenced by more than one input. That benefit offsets the drawback of less discoverability. modules.nixos.* is an intermediate, not an output. What is a benefit you get from having that intermediate get multiple definitions? How is the drawback of less discoverability offset?

That’s fair, but this post and the accompanying repository is also serving — perhaps unintentionally — as a marketing pitch for people like me who had ignored previous dendritic pattern ads as ā€˜flake stuff’. This post made me actually look at what the pattern is, and now I have questions. Others may have similar questions. You may or may not care to do anything about our questions; your prerogative.

1 Like

I do care, a lot! Yesterday I was just so much tired to invest more brain power at it. I’ve been documenting the Dendritic pattern as I find its benefits

and yes, it is unfortonate to me that my non-flakes experiment was taken as reference by many people new to the pattern. But I’m happy that people are having questions and wondering what Dendritic is all about -after all it is for all you people that I have been documenting and creating dendritic libs-

I’m now trying that the dendritic repo has a proper Flagship Example we can have when talking about the pattern, instead of my non-flakes experiment or the more advanced/complicated personal infras we have.

I also invite all people having questions to join the conversation at matrix or GH discussions (see the dendritic README) for links. Hopefully more people is reading those channels, so that I’m not the only one answering (I can be baised by my particular experience)