Confusing (non) domination of `with` statements

After debugging some problems with overrides in mach-nix, I noticed that the with expression in nix doesn’t behave at all as I assumed.

Given this expression:

set1 = { a = 1; };
set2 = rec {
  a = 2;
  b = with set1; a;

I expected set2.b to evaluate to 1. But in fact the result is 2.

That means the attributes brought into scope by with are overridden by the outer rec expression.

The nixos wiki doesn’t mention that behaviour.
The nixos manual mentions that
The bindings introduced by with do not shadow bindings introduced by other means, e.g.

let a = 3; in with { a = 1; }; let a = 4; in with { a = 2; }; ...

is equal to:

let a = 1; in let a = 2; in let a = 3; in let a = 4; in ...

After playing around with it a bit, I came to the conclusion that with statements dominate previous with statements but they do not dominate rec or let statements.

In my opinion this makes reading larger nix expressions more difficult than necessary. Wouldn’t it be much simpler if always inner expressions shadow outer expression no matter which type?

At least I would expect with to either be dominant or not dominant. But currently it’s a weird mix of being dominant against other with’s but not against rec and let.

I’m wondering if there is a good reason for this which I’m not able to see.

1 Like

I think that the fundamental reason for with not overriding let and rec is to preserve lexical scoping. There’s no problem with inner with overriding outer with, because inner let also overrides outer let, and inner rec also overrides outer rec.

How about we put it this way: in the current scheme, you can desugar a Nix expression using with into one without with, while not knowing what attributes each runtime value has. Sketch:

  • Whenever a with s; e is encountered, save the set s by transforming it into let q = s; in e, and also transforming in s and e if they use with.
  • Whenever a variable v is referred to, if there’s a lexical binding that binds it (parameter, let, rec is all i can think of), let v refer to that. Otherwise, consider all enclosing with clauses and let q0, q1, …, qn be the sequence of ‘saved’ sets, (see step 1), in order from outer to inner, then v shall refer to (q0 // q1 // ... // qn).v.
  • Handle other expressions.

Note that the preceding transformation makes no reference to runtime behavior.

That certainly sounds desirable to me: the behavior of variables and scope is not subtlely altered by runtime values. Breaking lexical scoping is probably one of the big reasons that JavaScript’s with is unpopular and eventually deprecated (citation needed). Seems like scope-affecting with was deemed unwanted by the community? What do you think?