Conditionally set attribute

I want something along the lines of

{
    a = 1;
    b = <if stars align and enough goats have been sacrificed> 2;
    c = 3;
}

to evaluate to one of

  • { a = 1; b = 2; c = 3; }
  • { a = 1; c = 3; }

depending on whether the stars have aligned and enough goats have been sacrificed.

In the past I have used all sorts of ugly and long-winded hacks to implement such things, and I’d like to start using some clean, canonical mechanism … and it’s taking me far too long to find a satisfactory answer in the docs.

What is a sensible way of doing this sort of thing?

If you can use lib from nixpkgs the most common way would be the first example, if you haven’t, you had to use the second (or hide behind a function yourself):

{a = 1; c = 3} // lib.optionalAttrs cond {b = 2;};

# ---

{a = 1; c = 3} // if cond then {b = 2;} else {};

Hiding it behind a function myself is the approach I’ve been taking so far.

But a solution will always involve // for sets and ++ for lists?

I find the signal to noise raito of something like this

{
  this = foo;
  that = bar;
} //
when tuesday { tuesday-stuff = baz; } //
{ other = zot; } //
when in-rome { roman-stuff = quux; } //
{ further = quuux; }

significantly worse than something like this

{
  this = foo;
  that = bar;
  tuesday-stuff = (when tuesday baz);
  other = zot;
  roman-stuff = (when in-rome quux);
  further = quuux;
}

and analogous situations for lists rather than sets.

Has someone come up with an EDSL for mitigating the noise in these situations? In Lisp or Rust I’d be reaching for a macro.

Is lib.mkIf relevant to this discussion?

No.

Make it return null or another “magic” value that signals “inexistence” and use an additional lib.filter or lib.filterAttrs.

1 Like

Oh, this will do very nicely.

How should I create an inexistence value that cannot be confused with emptiness / falsity / null values that might have other meanings?

Depends on the domain. There is no omnitrue answer.

I basically need an unique symbol that I know nobody else will use in their libraries. With algebraic data types, Maybe or Option would do the trick; in dynamic languages I’d use gensym in Lisp, or create a new type with a single instance in Python (or just use my_none = object()). I just need a mechanism for creating something that is guaranteed to be unequal to anything that already exists.

It is if you’re using the module system! In the module system mkIf is exactly what you’re asking for:

# test.nix
{ cond }:
(import <nixpkgs/lib>).evalModules {
  modules = [
    ({ lib, ... }: {
      options.foo = lib.mkOption {
        type = lib.types.attrsOf lib.types.int;
        default = {};
      };

      config.foo = {
        x = 0;
        y = lib.mkIf cond 1;
      };
    })
  ];
}
$ nix-instantiate --eval --strict test.nix -A config.foo --arg cond false                                        
{ x = 0; }                                                                                                          
                                                                                                                    
$ nix-instantiate --eval --strict test.nix -A config.foo --arg cond true                                         
{ x = 0; y = 1; }

Additionally there’s this hacky almost-solution, which works because dynamic attribute keys with null don’t get set!

# test.nix
{ cond }:
{
  x = 0;
  ${if cond then "y" else null} = 1;
}
$ nix-instantiate --eval --strict test.nix --arg cond false                                                      
{ x = 0; }                                                                                                          
                                                                                                                    
$ nix-instantiate --eval --strict test.nix --arg cond true                                                       
{ x = 0; y = 1; }
3 Likes

This is agonizing. I’ve been wanting (and not managed) to find the time to grok the module system for ages. The temptation to spend time on it now is huge, but I’m getting too many levels away from the immediate problem that I need to solve … soon.

One thing: I came to Nix for the statelessness, so anytime I see <nixpkgs ...> I run away in terror; can your test.nix example somehow be made more flaky?

Make it use the nixpkgs flake instead.

{ cond }:
let
  nixpkgs = builtins.getFlake "github:nixos/nixpkgs/af21c31b2a1ec5d361ed8050edd0303c31306397";
in
nixpkgs.lib.evalModules {
   ...
}

The current state of Nix is far from ideal, but considering that Flakes is still experimental and will likely change considerably in the future, I prefer recommending stable ways of pinning Nixpkgs instead:

let
  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/af21c31b2a1ec5d361ed8050edd0303c31306397";
  lib = import (nixpkgs + "/lib");
in lib.evalModules {
  ...
}

I like this hack more than the hack with magic values and filters. Yoinked.

1 Like