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:
- If it’s lexically bound, leave it as is.
- Otherwise, if there are no
with
constructs above it, report an unbound variable. - Otherwise, take all the
with A;
that wraps this variable, combine them together like(A1 // A2 // A3)
, and change the variablev
into((A1 // A2 // A3).v)
Some examples:
Most common usage: Just let me type less stuff:
# Before [ pkgs.foo pkgs.bar pkgs.baz ] # After with pkgs; [ foo bar baz ]
Lexical scoping is preserved
# Before let a = 4; in with { a = 3; b = 4; c = 5; }; a + b + c # After let a = 4; in a + ({ a = 3; b = 4; }.b) + ({ a = 3; b = 4; }.c)
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.
About this document
This document was generated using Pandoc from the Markdown source:
$ pandoc -f markdown --mathjax -t html < nix-lang-with.md > index.html