query
On this page

zipAttrsWith

lib.attrsets.zipAttrsWith

Primop
Docs pulled from | This Revision | 12 minutes ago


Nixpkgs manual

Merge sets of attributes and use the function f to merge attribute values. Like lib.attrsets.zipAttrsWithNames with all key names are passed for names.

Implementation note: Common names appear multiple times in the list of names, hopefully this does not affect the system because the maximal laziness avoid computing twice the same expression and listToAttrs does not care about duplicated attribute names.

Type

zipAttrsWith :: (String -> [a] -> b) -> [{ [String] :: a }] -> { [String] :: b }

Examples

lib.attrsets.zipAttrsWith usage example

zipAttrsWith (name: values: values) [{a = "x";} {a = "y"; b = "z";}]
=> { a = ["x" "y"]; b = ["z"]; }

Nix manual

Takes 2 arguments

f, list

Transpose a list of attribute sets into an attribute set of lists, then apply mapAttrs.

f receives two arguments: the attribute name and a non-empty list of all values encountered for that attribute name.

The result is an attribute set where the attribute names are the union of the attribute names in each element of list. The attribute values are the return values of f.

builtins.zipAttrsWith
  (name: values: { inherit name values; })
  [ { a = "x"; } { a = "y"; b = "z"; } ]

evaluates to

{
  a = { name = "a"; values = [ "x" "y" ]; };
  b = { name = "b"; values = [ "z" ]; };
}

Time Complexity

O(N * log k) where:

N = total attributes across all sets k = number of unique keys across all sets

Noogle detected

Aliases

Implementation

This function is implemented in c++ and is part of the native nix runtime.

src/libexpr/primops.cc:3632

static void prim_zipAttrsWith(EvalState & state, const PosIdx pos, Value ** args, Value & v)
{
    // we will first count how many values are present for each given key.
    // we then allocate a single attrset and pre-populate it with lists of
    // appropriate sizes, stash the pointers to the list elements of each,
    // and populate the lists. after that we replace the list in the every
    // attribute with the merge function application. this way we need not
    // use (slightly slower) temporary storage the GC does not know about.

    struct Item
    {
        size_t size = 0;
        size_t pos = 0;
        std::optional<ListBuilder> list;
    };

    std::map<Symbol, Item, std::less<Symbol>, traceable_allocator<std::pair<const Symbol, Item>>> attrsSeen;

    state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.zipAttrsWith");
    state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.zipAttrsWith");
    const auto listItems = args[1]->listView();

    for (auto & vElem : listItems) {
        state.forceAttrs(
            *vElem, noPos, "while evaluating a value of the list passed as second argument to builtins.zipAttrsWith");
        for (auto & attr : *vElem->attrs())
            attrsSeen.try_emplace(attr.name).first->second.size++;
    }

    for (auto & [sym, elem] : attrsSeen)
        elem.list.emplace(state.buildList(elem.size));

    for (auto & vElem : listItems) {
        for (auto & attr : *vElem->attrs()) {
            auto & item = attrsSeen.at(attr.name);
            (*item.list)[item.pos++] = attr.value;
        }
    }

    auto attrs = state.buildBindings(attrsSeen.size());

    for (auto & [sym, elem] : attrsSeen) {
        auto name = Value::toPtr(state.symbols[sym]);
        auto call1 = state.allocValue();
        call1->mkApp(args[0], name);
        auto call2 = state.allocValue();
        auto arg = state.allocValue();
        arg->mkList(*elem.list);
        call2->mkApp(call1, arg);
        attrs.insert(sym, call2);
    }

    v.mkAttrs(attrs.alreadySorted());
}

Implementation

The following is the current implementation of this function.

zipAttrsWith =
    builtins.zipAttrsWith or (f: sets: zipAttrsWithNames (concatMap attrNames sets) f sets);