Nix if-then-else expressions

Nix if-then-else expressions

A coworker asked the following question:

I understand how to use an if/else, but I don’t understand how to get an if/elseif/else.

Below is my Slack response (slightly edited) to explain the dissonance in the question itself and then provide a path forward for him. I imagine this might be a common misunderstanding for those coming to Nix without prior understanding of expression languages vs languages that use statements.

Nix being an expression language just has expressions (where the result of each part evaluates to a value [eventually]). Most mainstream imperative languages like Perl, Bash, etc. that most people are familiar with use this notion of statements statements rather than expressions. Statements allow these languages to support if/elseif/…/else as an extension.

In Nix and other expression based languages, this is not the case and a limiting factor of expressions is that every expression must evaluate to a value. So you might write:

  key = if builtins.pathExists ./path then "woot" else "bummer";

In the case the ./path exists it will evaluate to the value "woot" otherwise it evaluates to the value "bummer".

So the result of the top level expression is:

{ key = "woot"; }

# OR

{ key = "bummer"; }

This does not translate to languages that model ifs as statements, for example:

$ declare bla=$(if true; then "bla"; else "foo"; fi)
$ echo "${bla}"

$ declare bla=$(if true; then echo "bla"; else echo "foo"; fi)
$ echo "${bla}"

Note: that side effects are required inside each clause.

Looking at the equivalent if/else statement for the expression example above we have:

if [ -f ./path ]; then
  declare key="woot"
  declare key="bummer"

Here you see that Bash uses side effects to do the assignment in each case, but say we had this:

if [ -d ./path ]; then
  declare key="woot"
elseif [ -x ./path ]; then
  echo "executable"
  declare key="bummer"

Now we have a case (where the file is not a directory and also executable) that the variable key is not set. This wouldn’t happen in an expression based language.

In short if-then-else is the only way we can build an expression to always evaluate to a value where all logical paths are covered without the program needing to know about the underlying data or condition clauses inspected in the if portion.

You can think of if-then-else as a lambda that is defined as:

  ifThenElse = cond: t: f: if cond then t else f

So to solve the problem you take one of two approaches.

{ envType, defaultCfg }:
  cases = { "dev" = devCfg; "test" = testCfg; "prod" = prodCfg; };
  lookup = attrs: key: default:
    if attrs ? key then attrs."${key}" else default;
in lookup environments envType defaultCfg