The thing about shadowing is that `with` will not shadow anything that's in the implicit context, which includes things defined by `let` (hence why I avoid it) and function arguments (unavoidable). We can shadow other `with` bindings as much as we like, e.g.
with { foo = 1; };
with { foo = 2; };
with { foo = 3; };
with { bar = with { foo = 4; }; foo; };
[ foo bar ]
This evaluates to [ 3 4 ], since later bindings to `foo` will shadow/override earlier ones.
This doesn't work for bindings that are in the implicit environment/context, for example:
with {
f = foo: with { foo = 42; }; foo;
};
f 123
This evaluates to 123, since the binding `with { foo = 42; }; ...` doesn't shadow/override the argument `foo`.
The rationale for this behaviour is to avoid changes made to an attrset from overriding explicit arguments. For example:
foo:
with bar;
x + foo
This is presumably meant to behave like `foo: bar.x + foo`, but a future change to `bar` might include a `bar.foo` attribute, in which case we don't want the above to change behaviour to `foo: bar.x + bar.foo`.
I understand this rationale, especially when using huge, constantly-changing attrsets like `with nixpkgs; ...`, but it's still annoying ;)
To make it clearer, he is saying that he does
with { foo = "bar"; }; <some other block>
instead of
let foo = "bar"; in <some other block>
The differences here so other people can follow along.
1. with will not shadow variables defined higher up. This can be either desirable or very confusing.
2. let is recursive by default, meaning you can reference other variables in that block. This behavior can be mimicked with `with rec {}`