Skip to content

Aspects & Functors

import { Aside } from ‘@astrojs/starlight/components’;

In Nix, any attribute set with __functor can be called as a function:

let
counter = {
value = 42;
__functor = self: n: self.value + n;
};
in counter 8 # => 50

The __functor receives self (the attrset) and an argument.

Every aspect in flake-aspects has a default __functor:

{
nixos = { a = 1; };
__functor = self: _context: self; # ignores context
}

By default, it ignores context and returns itself. But you can replace __functor with one that inspects context:

{
nixos.foo = 24;
__functor = self: context:
if context ? host
then self
else { includes = [ fallback ]; };
}

Aspects have three kinds of attributes:

Direct class settings:

den.aspects.igloo = {
nixos.networking.hostName = "igloo";
darwin.nix-homebrew.enable = true;
homeManager.programs.vim.enable = true;
};

Dependencies on other aspects — a directed graph:

den.aspects.igloo.includes = [
den.aspects.gaming
den.aspects.tools._.editors
({ host, ... }: { nixos.time.timeZone = "UTC"; })
];

Includes can be:

  • Static aspects — always included
  • Functions — called with context, included when they match

Nested sub-aspects forming a tree:

den.aspects.gaming._.emulation
den.aspects.gaming.provides.emulation = {
nixos.programs.retroarch.enable = true;
};

When Den needs a NixOS module from an aspect, it calls .resolve:

module = den.aspects.igloo.resolve {
class = "nixos";
aspect-chain = [];
};
graph TD
  Asp["den.aspects.igloo"]
  Asp --> Owned["owned: nixos, darwin, homeManager"]
  Asp --> Inc["includes: gaming, tools._.editors, fn..."]
  Asp --> Resolve[".resolve { class = 'nixos' }"]
  Resolve --> Collect["Collect all 'nixos' attrs<br/>from aspect + transitive includes"]
  Collect --> Module["Single merged Nix module"]

This collects all nixos configs from the aspect and all its transitive includes into a single Nix module.

Functions in includes are context-aware:

den.aspects.igloo.includes = [
# static — always included
{ homeManager.programs.direnv.enable = true; }
# parametric — called with context
({ host, ... }: { nixos.time.timeZone = "UTC"; })
# conditional — only called when context has user
({ host, user, ... }: {
homeManager.programs.git.userName = user.userName;
})
];

Den inspects each function’s argument names to decide whether to call it with the current context. See Parametric Aspects for the matching rules.

den.default is an aspect with parametric.atLeast functor:

den.default = den.lib.parametric.atLeast { };

It dispatches context to all its includes. Since every host, user, and home includes den.default, it serves as the backbone for global routing.