What is the added benefit of `let` / `in` compared to plain `with` / `with rec`

looking through code to find best practices, it looks like abandoning let/in syntax and using plain with is simplifying and hence improving readability of nix-code. Take a look

with pkgs;
let
  a = 1;
  b = 3;
in
  a + b;

vs

with pkgs;
with {
  a = 1;
  b = 3;
};
a+b;

The second uses less idioms and makes clear that you can think of let just as an alternative way of writing with statements. The with statements have the added benefit that recursive references need to be made explicit by adding the rec keyword.


That brings me to the question, why let/in was at all added to the language. Is there a benefit of using it instead of plain with statements?

1 Like

A random with massively pollutes your namespace and introduces random names.

In general I prefer let inherit …; in … over with.

And if I use with, then I try to have the scope it affects as little as possible.

I have never considered using with { … }; …; so far, as I consider let/in semantically better for introducing local bindings.

thank you @NobbZ for your answer. Unfortunately it is hard for me to understand what you wanted to say. Maybe you can clarify your perspective a bit more.

The semantics of with is precisely to add local bindings. That is all it does. In the usage which is of focus here, it does pollute my namespace exactly the same way as if I would use let/in, i.e. it does not pollute it at all.

With these thoughts it is really hard for me to understand how you come to your conclusion that let/in is semantically superior to with?
For me it rather seems they are both completely semantically sound.

with pkgs does massively pollutes in a way you can’t do with let.

Semantically with is much closer to import from Haskell, while let is, well, let.

sure, with pkgs is one standard usage I just included as a common example which was meant to illustrate why let/in seems somehow added. And I want to know why.

So now I guess I understood that you prefer let/in because you are not able to do such dynamic namespace bindings. It is simpler and hence superior. That is your thinking if I understood it correctly.

Put the other way arround, with is strictly more powerful than let/in as you can bind variables to the local namespace without naming them explicitly. So the argument could go like, do we need with at all? and the community seems to have agreed, that it is a nice and powerful feature to have. At least I have seen it quite a lot. Hence I was assuming that the with statement is kind of given.


So given that the more powerful with statement is a fundamental part of the language, we get back to the original question what let/in would add compared to with in the reduced example.

let
  a = 1;
  b = 3;
in
  a + b;

vs

with {
  a = 1;
  b = 3;
};
a + b;

Here both are explicitly naming the bindings to the local namespace, so the argument of polluting does not apply here. What is the benefit of let/in?

with foo is considered a bad design error of the language by some of us, and we consider the excessive use of with pkgs throughout tutorials and examples not only unidiomatic but also misleading.

Also, remember that let and function arguments win over with.

foo: let foo = 1; in foo always returns 1, while foo: with { foo = 1; }; foo is equivalent to x: x.

6 Likes

impressive detail of the language. I was not aware that function arguments are stronger than with bindings. I guess this was chosen exactly because of the dynamic power of with. Quite unintuitive and surprising if you use static bindings indeed.

Thank you also for showing the alternative best practice of not using with at all. I am going to think more about it.

2 Likes

If I could give my take, rec is a token intended to give attribute sets recursive powers and reference their own contents, so I wouldn’t necessarily bar it from usage, but I wouldn’t use it for any attribute set that could easily suffice with a simple static let binding. To my mind, unecessary recursion is extra computation, however trivial (and sometimes not as much so), that just isn’t needed unless your usecase genuinely requires introspective knowledge of an attributes sets own contents to function efficiently.

As a generalized example, I have one attribute set of lists which references it’s own lists to make bigger lists of itself. I could do this with a function, but for this particular collection rec was the most expressive and straight forward way to solve the particular problem.

This isn’t the same kind of problem as simply setting the version and name of a package, and using that information in a few different places.

Another point, I may never have thought about it this deeply if I didn’t run into infinite recursion using rec in overlays that were easily resolved with a let binding.

Also didn’t mention with because @NobbZ’s argument seems sufficient. I typically use with for pkgs and nothing else.

1 Like

I like comparing with x with import * from x in python, which has also very much grown out of favor.

Polluting namespaces is a problem because you might end up accidentally writing to or using something you didn’t realize existed, turning a missing variable error into a sneaky horror.

It also makes it just harder to read the code. It’s harder to understand what an expression is trying to do if you see with lib; instead of let inherit (lib) toKeyValue; in ..., especially if there are a bunch of functions already in scope. Yes, the former is more concise, but it doesn’t convey any actual meaning.

Hence it’s starting to be seen as a design flaw, much like similar expressions in other languages :slight_smile:

I’ve been working quite some time in Scala 2 where import mypackage.* was indeed very often used. The reason this did not pollute the namespace in malicuous ways was that the Editor gives you complete support about which is which.

With dynamic languages like python or nix this is more difficult unfortunately. I guess if there would be a nix IDE with good jump-to-source support it wouldn’t be that much of a trouble for people to use it.

I’d still consider that bad practice, for the aforementioned readability reasons and because locking people into one IDE is not a great thing, especially on a FOSS project.

It does depend a bit on the package, not all style nudges need to always be followed. For some packages it makes little sense without; In Rust ::prelude:* is a common way to denote that. But I don’t think this applies for something as filled with random things as pkgs or lib.

An IDE can definitely not save you from all errors resulting from a polluted namespace either, detecting accidental-use without constant false positives would need some gnarly heuristics, even if it’s less common.

That said though, nix-language-server does exist and is worth trying out :slight_smile:

3 Likes

It’s not about just function arguments. Looks like a binding (whether it was introduced as a function argument or with a let ... in expression) is always stronger then importing variables into the local scope using with:

nix-repl> let foo = 2; in with { foo = 3; }; foo
2

So importing variables using with doesn’t introduce new variable if it already exists in the local scope.

3 Likes