Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation: guide for using let in vs rec vs finalAttrs #315337

Open
3 tasks done
isabelroses opened this issue May 28, 2024 · 11 comments
Open
3 tasks done

Documentation: guide for using let in vs rec vs finalAttrs #315337

isabelroses opened this issue May 28, 2024 · 11 comments

Comments

@isabelroses
Copy link
Member

Problem

I often see maintainers and committers alike suggesting to use the let in syntax following the practices suggested by https://nix.dev/guides/best-practices.html. However, this is hardly uniform some would suggest using rec, and it's hard to get a solid grasp on what to use in each situation. This was only exemplified when finalAttrs started popping up more commonly as of recent.

Proposal

From my understanding https://nix.dev is an official source so perhaps it would be best to get everyone on board and aware of this best practice? Or maybe to get a more concrete set of best practices to use in each given situation. Or at the very least make members aware that the let in syntax is said to be the best practice, as stated in https://nix.dev/guides/best-practices.html.

Checklist


Add a 👍 reaction to issues you find important.

@Atemu
Copy link
Member

Atemu commented May 30, 2024

At the very least I'd like reviewers to stop nitpicking between these.

Unless there is explicit community-wide consensus (i.e. RFC), wide-spread historical precedent (i.e. mkDerivation attr order) or significant concrete problems with the code (i.e. correctness, readability), people should be free to use whichever code patterns or organisation they like.

And by significant, I really do mean significant. Code being organised against your personal preference or patterns you dislike being used are minor differences in taste, not a significant readability problems.

Nitpicking such "issues" is not in any way productive and utterly confusing/unwelcoming to newcomers. They are faced with dozens such implicit rules that aren't explicitly written down anywhere and, even worse, differ wildly depending on which person reviews the code. I don't doubt we've lost more than a dozen contributors whose first PR was faced with 12 nitpicks and 0 actually substantial review comments and then left to bitrot for months.

I've been guilty of this aswell and we all just need to stop doing it.

@pluiedev
Copy link
Contributor

pluiedev commented May 30, 2024

I was (and still am, I guess) one of those nitpickers and I think what really needs to happen here is to standardize the finalAttrs pattern so that it's not only applicable to mkDerivations. Nowadays I don't even nitpick recs appearing in other builders and change them to let-ins, but when I didn't change that in one of my own PRs I got nitpicked for that 😅

It almost feels pedantic, in a way, with the rule being more of a dogma, and the rationale behind it not being at all clear to new contributors and the applicability of it still very limited. We should really try to explain things more, come to a common standard, and make that standard applicable to everything.

Perhaps a table could serve as a brief explanation as to why one pattern may be preferred over the other?

Pattern Overridability Readability Applicability
rec ❌ Does not automatically respond to overrides ❌ Recursive variables could be hard to spot and locate in a large file ✔️ All builders
let in ❌ Does not automatically respond to overrides ✔️ Variables are clearly defined in the let portion, and used in the in portion ✔️ All builders
finalAttrs ✔️ Automatically responds to overrides ✔️ Recursive variables are explicitly prefixed with finalAttrs or inherited from finalAttrs ❌ Only mkDerivation

Also, the existence of finalAttrs is buried in a tiny chapter in the huge Nixpkgs manual, which nobody would even know of were it not for the recent surge of its use. I think there should also be more coverage of this pattern elsewhere in the scattered Nix docs, so that new contributors could be made aware of it before they start contributing. The explanation of the differences between rec and finalAttrs is also terribly unclear and not backed up with concrete examples, like so:

let 
  withRec = stdenv.mkDerivation rec {
  	pname = "with-rec";
  	version = "0.1.0";
  	
  	src = fetchFromGitHub {
  	  owner = "example";
  	  repo = "example";
  	  rev = "v${version}";
  	  hash = "...";
  	};
  };
  withFinalAttrs = stdenv.mkDerivation (finalAttrs: {
  	pname = "with-rec";
  	version = "0.1.0";
  	
  	src = fetchFromGitHub {
  	  owner = "example";
  	  repo = "example";
  	  rev = "v${finalAttrs.version}";
  	  hash = "...";
  	};
  });
in
lib.trace (withRec.overrideAttrs { version = "0.2.0"; }).src.rev # v0.1.0
lib.trace (withFinalAttrs.overrideAttrs { version = "0.2.0"; }).src.rev # v0.2.0
{};
`` 

@getchoo
Copy link
Member

getchoo commented May 30, 2024

i think #310373 is important to bring up in this conversation.

at first glance, finalAttrs does seem like a "silver bullet" to overrides propagating across the expression. however, continuing from the example above:

lib.trace (withRec.overrideAttrs { version = "0.2.0"; }).src.rev # v0.1.0
lib.trace (withFinalAttrs.overrideAttrs { version = "0.2.0"; }).src.rev # v0.2.0

while the rev of src is overridden through finalAttrs, hash isn't. as explained in the issue, this and other uses of finalAttrs can easily introduce a lot of footguns to users when overriding; and even in the best case, the use of finalAttrs over rec here doesn't really improve anything for the end user

At the very least I'd like reviewers to stop nitpicking between these.

this is probably what i want most out of this issue, though. a lot of times the differences between these patterns are exactly that: nitpicks. i'm sure it's frustrated many here time and time again - and if it doesn't actually benefit our user in many cases, why are we doing it? why are we giving new contributors a hard time over preferred patterns to reach the same goal?

i would personally encourage package maintainers to use their personal preference unless it's actually affecting how the expression is used or defined. for other readability/consistency concerns, we will hopefully have nixfmt soon tree-wide soon 😄

@pluiedev
Copy link
Contributor

The problem with version specifically, I think, is that it's either easy to forget to update the hash if the rev depends on finalAttrs.version, or it's easy to forget to update the source altogether. Might be better to have a built-in lint/warning/assert against that in mkDerivation

@isabelroses
Copy link
Member Author

isabelroses commented May 30, 2024

It may also be worth reading over #293452 and #119942, for more insight.

@Atemu
Copy link
Member

Atemu commented May 30, 2024

@getchoo made the same points I was going to make.

To that I want to add that whenever this comes up, one should think about which parts of the package they want to expose as an external API and what parts you don't.

finalAttrs responds specifically to overrideAttrs, not overrides in general. Whether you want to use the pattern depends on whether you want the reference attributes to be be generically overrideable; part of the package's API. It does not always make sense to do this however. As @getchoo pointed out, exposing the version as part of the API using finalAttrs like this has issues.

As a more robust alternative, I've recently discovered that you can simply make such parameters part of the regular override interface. I think this is even better as it makes them explicit part of the package's external API and allows other parts of the external API to respond to version changes (i.e. withFoo ? lib.versionAtLeast version "6.1"). I did this in the ffmpeg package and I think it's better for it: https://github.com/NixOS/nixpkgs/pull/295691/files#diff-547e4d5699df1c405afdc0c2837df95959ed6de64f384ca5720ee37367eebcce
This design allows you to take any ffmpeg variant (i.e. ffmpeg_7-full) and transform it into any other (i.e. ffmpeg_4-headless) using only override, without any overrideAttrs.

Regardless of opinions on these manners though, a table like @pluiedev's would be great to have in the contributor's manual. We generally need more such short summaries and general guidelines to be written down in accessible places.

I also want to mention that, in my book, it is perfectly valid to use rec, let ... in and finalAttrs in combination as they each have distinct purposes.

@nixos-discourse
Copy link

This issue has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/more-official-guidelines-in-regards-to-styling-choices/48407/3

@Mic92
Copy link
Member

Mic92 commented Jul 7, 2024

Perhaps a table could serve as a brief explanation as to why one pattern may be preferred over the other?
Pattern Overridability Readability Applicability
rec ❌ Does not automatically respond to overrides ❌ Recursive variables could be hard to spot and locate in a large file ✔️ All builders
let in ❌ Does not automatically respond to overrides ✔️ Variables are clearly defined in the let portion, and used in the in portion ✔️ All builders
finalAttrs ✔️ Automatically responds to overrides ✔️ Recursive variables are explicitly prefixed with finalAttrs or inherited from finalAttrs ❌ Only mkDerivation

What's missing in this table is the performance overhead of finalAttrs. Also I probably should write proper benchmark for this, a performance decrease have been observed by several people because of the fixed input computation.

@eclairevoyant
Copy link
Contributor

btw, finalAttrs also applies to the nim and php builders, perhaps some others as well that I don't know offhand.

@pluiedev
Copy link
Contributor

pluiedev commented Jul 9, 2024

btw, finalAttrs also applies to the nim and php builders, perhaps some others as well that I don't know offhand.

It's kinda part of the problem, isn't it? It's very inconsistent as to which builders have the fixed-point capability...

@eclairevoyant
Copy link
Contributor

Someone has to care enough to add it to the other builders, basically. Or redo the entire fixed-point concept from scratch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants