NixOS / nix

Nix, the purely functional package manager
https://nixos.org/
GNU Lesser General Public License v2.1
12.8k stars 1.52k forks source link

`removeAttrs`: allow removals to be specified as attrset, and conversely for `intersectAttrs` #9050

Open roberth opened 1 year ago

roberth commented 1 year ago

Is your feature request related to a problem? Please describe.

I'd expect the following to work,

lib.removeAttrs { a = 1; b = 2; } { "b" = throw "ignored"; } 
 == builtins.intersectAttrs ["a"] { "a" = 1; }

but instead I have to write this:

lib.removeAttrs { a = 1; b = 2; } [ "b" ]
 == builtins.intersectAttrs { "a" = throw "ignored"; } { "a" = 1; }

This is arbitrary. Let's just make it work.

Describe the solution you'd like

The second argument of removeAttrs is allowed to be an attrset where the values are ignored.

The first argument of intersectAttrs is allowed to be a list where the items are interpreted as attribute names.

Describe alternatives you've considered

Additional context

Priorities

Add :+1: to issues you find important.

infinisil commented 8 months ago

Not a fan of this, because:

Imo the path forward is to leave the builtins alone and instead implement better functions in lib, e.g.:

Such that a == lib.attrsets.unionOfDisjoint (removeKeys k a) (selectKeys k a).

roberth commented 8 months ago
  • Builtins don't need to have a good user interface

True, but implemented in C++ would perform slightly better.

  • In general it sounds like a design smell for functions to only use parts of their arguments.

Should be somewhat expected in a lazy language, but I'm biased; I'm rather comfortable with laziness. That's rare.

removeKeys

We never use "keys". It's always something like attributes, attrNames, attrs.

List ->

Conversions to list tend to allocate more, and the algorithms can't take advantage of the list's properties, because it has basically none.

infinisil commented 6 months ago

The performance argument is a really good one.

Here's another thought: These two operations (selecting attributes and removing attributes) can probably be done more efficiently together (which is often also what you actually need). So how about something like this:

builtins.diffAttrs { a = 0; b = 0; } { b = 1; c = 1; }
-> {
  only.left = { a = 0; };
  both.left = { b = 0; };
  both.right = { b = 1; };
  only.right = { c = 1; };
}

This does cost extra attribute allocations (though this could be optimised with an idea like https://github.com/NixOS/nix/issues/7676). However this cost is constant, compared to the cost of having to run both intersectAttrs and removeAttrs.

roberth commented 6 months ago

That's a lot like a zipAttrsWith, but limited to two arguments.

Another option is

combineAttrs (name: left: left) (name: left: right: left + right) (name: right: right) { a = 0; b = 1; } { b = 10; c = 20; }
{ a = 0; b = 11; c = 20; }

or if you want more flexibility in the style of lib.concatMapAttrs

mergeZipAttrs (name: left: { ${name} = left; }) (name: left: right: { ${name} = left + right; ${name}-left = left; ${name}-right = right; }) (name: right: { ${name} = right; }) { a = 0; b = 1; } { b = 10; c = 20; }
{ a = 0; b = 11; c = 20; b-left = 1; b-right = 10; }

Idk what's better.