emanueljg.com

The clean namespace


2025-08-22 | Nix


It's common for software to have a lot of footguns, especially if it's flexible and powerful software. Nix, however, overwhelms you with entire keywords that are dangerous to use: rec and with. This is an explanation on what they do, why you should avoid them and better ways to do what they do (without sacrificing DRY or elegance!).

rec

rec creates a recursive attribute set.
      
attrs = let 
  a = "fi"; 
  b = "bur"; 
in rec {
  a = "foo";
  b = "bar";
  y = a + b;
}
    

It's commonly used across all of Nix. It lets attributes in an attribute set refer to each other.

First of, why is it bad? Well, I think it's personally quite annoying to read implicit recursion (explicit alternatives are covered later). It's also easy to accidentally create infinite recursion with rec, where an attribute depends on itself. It means that given any referenced value in a recursive attribute set, you always have to look through the entire attrset to check whether the value comes from the attrset or an outer binding. This is very annoying. But worse, rec creates namespace pollution. What does that mean?

Consider attrs in the first example. As you might've guessed, attrs.y evaluates to foobar, since the rec shadows the outer let-bindings a= "fi" and a = "bur". This leads to decisively unreadable structures and brittle evaluation. Imagine what happens in a refactor when you change the name of attrs.a = "foo" to attrs.aaa = "foo". Your y mysteriously takes on the value fibar because a remains a valid binding outside of the attrset. Easy mistake to make, hard mistake to troubleshoot.

This example attrset is a softball. 3 lines of primitive attributes with one line of outer bindings; it's trivial for pedagogical purposes. Now imagine instead of a and b you're using "attractive" attr names like name or default, which may hold important semantic importance in the outer bindings. Maybe you're 20, 30 attrs deep and you've scrolled past where the attrset's opening {. Maybe you're focused on things more important than namespace resolution. It's easy to see the code in the future blowing up because wires get crossed and mysterious values show up where they shouldn't.

There is a solution. In fact, there's multiple!

Starting off simple, you should generally prefer already paved roads for getting attrs when you can. Here follows a few example substitutions using already available bindings.


flake.nix

Your flake already gives you self in the outputs = { self, nixpkgs, ... }@inputs: function. Use it!

packages.${system} = {
  foopkg = pkgs.callPackage ./foopkg.nix { };
  default = self.packages.${system}.foopkg;
}
    

stdenv.mkDerivation

For a few derivation types, you can provide a finalAttrs-function (more on this later).
      
stdenv.mkDerivation (finalAttrs: {
  pname = "foo";
  version = "0.1.0"; 

  src = fetchFromGithHub {
    owner = ...
    repo = ...
    tag = "v${self.version}";
  };
})
    
In a mkDerivation context, it's convention to use finalAttrs instead of self for the self-referential function argument.

It's especially important to use finalAttrs: { ... } in derivations when you can in order to allow future overrideAttrs of the derivation to elegantly allow for overrides of version propagate to src.ref (assuming you also invalidate the hash). In old-style derivations, you have to explicitly override both version and src.ref (nixos/nixpkgs#315337).


If these "prefab" solutions aren't applicable to your current problem, you have two generic ones. Both of them are equally good, though one is certainly more common than the other.

let..in + inherit

      
let
  a = "foo";
  b = "bar";
in {
  inherit a b;
  y = a + b;
}
    
Intuitive, idiomatic and categorically better than rec. This is probably the most common solution to the problem and always a valid strategy.

lib.fix

The alternative solution is a bit more complicated: it utilizes lib.fix to create something called a fixed-point attribute set (similar to our stdenv.mkDerivation example)
      
attrs = lib.fix (self: {
  a = "foo";
  b = "bar";
  ab = self.a + self.b;
})
    
For the intellectually curious: as you might've guessed, stdenv.mkDerivation internally creates the fixed-point attrset which allows you to pivot to finalAttrs: { ... } without using lib.fix. Amongst others, rustPlatform.buildRustPackage does this too. To allow for your own mkDerivation-like function to use this feature, make sure to implement it using lib.extendMkDerivation (noogle docs). At the time of writing, there's not many derivation functions using this sadly.

let..in recursion

Though I personally dislike this technique as I consider it "hacky" self-reference... I am still presenting it in this post, as it's very common in the wild and it's better to know what it does.

attrs = let
  a = "foo";
  b = "bar";
  ab = attrs.a + attrs.b;
}; in
  attrs;
    
This might not be something you have thought of before, but let..in-blocks are actually inherently recursive in their scope, meaning attrs is a valid binding in let attrs = .... While this pattern is very common, I much prefer lib.fix as it's clearer to read the intent and flow of code. But it's still better than rec as the recursion is still explicit.

with

This one might be surprising to some people, considering how extremely common it is. with takes all attrs of an attrset and brings them into scope.
      
environment.systemPackages = with pkgs; [
  # pkgs.foo, pkgs.bar, pkgs.baz
  foo
  bar
  baz
]
    
You've probably seen this with pkgs construct a thousand times. Heck, even nixos-generate-config will give you a with-list of packages. It's also commonly used as with lib; in modules and the mkDerivation.meta attrset.

Despite how common it is, me and many other intermediate NixOS users consider it bad code. As with rec, it's bad due to namespace pollution, but in this case you're polluting it with (at the time of writing) ~25 000 attributes. It's especially bad considering OS packages are frequently flip-flopping between being taken from nixpkgs and being local "custom" derivations, meaning it's easy to have attribute shadowing.

What's the alternative? If you just have 1-4 attributes, I think you can just repeat pkgs. for them, it's not a lot of work (and with with lib;, this is actually the idiomatic approach) but if you want to maintain DRY in especially package lists, there is actually a really neat trick!

environment.systemPackages = 
  builtins.attrValues {
    inherit (pkgs)
      foo
      bar
      baz
    ;
  };
    
I'll be entirely honest and admit that I didn't understand the point of this pattern until recently and muttered to myself when I saw it: "you're still bringing the entirety of pkgs into scope with inherit, thereby polluting the namespace, so what's the point?" Yes, while that's true, I failed to consider that inherit obviously by nature doesn't allow for any non-pkgs attributes in the first place, thereby sidestepping the problem of flip-flopping attribute shadowing entirely. Clever!

environment.systemPackages = let
  foreign-bar = ...; 
  foreign-baz = ...; 
in builtins.attrValues {
  inherit (pkgs)
    foo
    # illegal!
    foreign-bar
  ;
  # ok!
  foreign-baz
}
    
Last but not least, I'll round this post off with a small exception to the outlawing of with: while I think that rec is categorically bad in any and all code, with actually has a very niche use case; composite NixOS option type declarations.
      
colors = lib.mkOption {
  type = with lib.types; 
    listOf str;
  ...
}
    
Why is with OK specifically here? Because it makes the type declaration read like English and makes it much easier to skim through options. Just be careful to not use it if you're inlining a lib.types.submoudle! If you use with lib.types; there, you're propagating the with to the entire submodule which brings us back to the same problem. In that case, either write all lib.types members explicitly or consume the submodule type from a let-binding.

<<< back to emanueljg.com    ^ Top


email: emanueljohnsongodin@gmail.com