dramforever

a row of my life

The with construct in nix-lang

2021-05-19

The Nix package manager comes with its own programming language for, among other things, defining packages. We’re not here to discuss whether that’s a good decision. We’ll call it the Nix language, or nix-lang for short.

This article assumes some familiarity with nix-lang. This is not a tutorial.

The syntax with A; E

Nix-lang has a construct with A; E. Its purpose is to bring the attributes of the attrset A into scope as variables within the expression E. So instead of:

[ pkgs.foo pkgs.bar pkgs.baz ]

You can write:

with pkgs; [ foo bar baz ]

As a syntax sugar, this has the obvious advantages of making code look shorter, and the obvious disadvantage of making it confusing.

‘The’ problem with with

A problem arises when there’s a conflict between a lexically bound variable (‘normal’ various bound by let or a lambda parameter) and something that’s bound by with:

let a = 4;
in with { a = 3; }; a

An obvious way to resolve this would be to make this expression evaluate to 3. This comes with a price though: Lexical scope would be broken. This is in fact the most commonly cited reason that an almost equivalent construct, with in JavaScript, is considered deprecated. (See MDN for example.)

Since we’re talking about Nix, let’s imagine that Nix-lang worked this way, with overriding normal variables. You have in your code:

let foobar = "something";
in with pkgs;
  /* ... */

And next month, a package called foobar is added to Nixpkgs. Your code would be broken.

Thankfully, that’s not what happens in nix-lang.

The solution in Nix-lang

In nix-lang, with does not override lexically bound variables. This example, in the real nix-lang, evaluates to 4:

let a = 4;
in with { a = 3; }; a

with simply never override something that’s lexically bound. with A; E only affects variables in E that are otherwise unbound.

A desugaring of with

This means that with in nix-lang is a purely syntactical construct. You can eliminate all uses of with in an expression without ever evaluating the code, because you don’t need to.

The only thing changed is that an unbound variable, which would be a syntax error, now becomes a reference to the attrset mentioned to with.

So, to desugar with, look at each variable mentioned in the code:

Some examples:

Well technically…

There’s a small mistake with the translation above. You can’t just copy verbatim the attrset used in with into each usage, because the with dictionary is only evaluated once. This hardly matters ever, but ideally, the latter example should be translated into something like: (Where __with_1 is a fresh variable)

let a = 4;
in let __with_1 = { a = 3; b = 4; c = 5; };
  in a + __with_1.b + __with_1.c

Imagine that instead of { a = 3; b = 4; c = 5; } there is a complicated computation. The naive translation would duplicate this computation, which is probably undesirable. This case occurs in the commonly used pattern:

with (import <nixpkgs> {}); /* ... */

So should we use with?

Be careful. The fact that with modifies the behavior of unbound variables instead of all variables is arguably an improvement over the now deprecated JavaScript with. But it still modifies the behavior of unbound variables.

Given the possibly confusing behavior, I personally only use with in certain circumstances in which I’m familiar with the consequences, like:

environment.systemPackages = with pkgs; [ foo bar baz ];

meta = with lib; { /* ... */ };

helperFunction = with builtins; /* use builtins here */

I don’t really like with (import <nixpkgs> {});, but admittedly, I sometimes get sloppy and use it.

Hopefully, equipped with a better understanding of what nix-lang’s with does, you know what you want to do with it.

I probably already annoyed you with all those with jokes. I’m going to stop.