Nesting let expressions

Hey everyone,

I’m trying to nest two let…in expressions in a script.nix configuration file which adds scripts and .desktop applications, but I have troubles with the syntax.

The idea was to abstract bash script parts to have a tidier script.nix file. I’ll give a useless basic example, keeping my basic structure:

# bash scripts parts to abstract
let
  shebang = "#!${pkgs.stdenv.shell}";
  errorExit = ''
    if [ $? -ne 0 ]
        then
        echo "error"
        read -n 1 -s -r -p "press any key to exit"
        exit 3
      fi
  ''
  properExit = ''
  echo "done"
  read -n 1 -s -r -p "press any key to exit"
  exit 0
  ''
in
{
  # create bash scripts
  let
    helloScript = pkgs.writeScriptBin "hello" ''
      ${shebang}
      echo "hello"
      ${errorExit}
      ${properExit}
    '';
    byeScript = pkgs.writeScriptBin "bye" ''
      ${shebang}
      echo "bye"
      ${errorExit}
      ${properExit}
    '';
    # create .desktop files
    helloDesktop =pkgs.makeDesktopItem {
      name = "hello";
      desktopName = "hello";
      icon = "/home/tcip/.script/icons/hello.png";
      exec = "${helloScript}/bin/hello";
      terminal = "true";
    };
   byeDesktop =pkgs.makeDesktopItem {
      name = "bye";
      desktopName = "bye";
      icon = "/home/tcip/.script/icons/bye.png";
      exec = "sudo ${byeScript}/bin/bye";
      terminal = "true";
    };
  in
  {
    # instantiate packages
    environment.systemPackages =
    [
      helloScript
      helloDesktop
      byeScript
      byeDesktop
    ];
    # add sudoers NOPASSWD option
    security.sudo.extraConfig = ''
      %wheel ALL=(ALL:ALL) NOPASSWD: ${byeScript}/bin/bye
    '';
  }
}

However – since I don’t know how to nest them :slightly_frowning_face: – I get this error:

error: syntax error, unexpected LET, at /etc/nixos/scripts2.nix:20:3
(use '--show-trace' to show detailed location information)
building Nix...

How can I properly write this – if it can be done?
Thanks and all the best,

Just remove the intermediate curly brace. The following is fine:

let a = 1;
in
let
b=2;
in a+b

1 Like

Thanks @tomberek for your soon answer :grinning:!
All the best,

bindings within the same let scope should be able to refer to each other, you should be able to make a single let scope

nix-repl> let a = 2; b = a + 1; in a + b
5
2 Likes

Thanks @jonringer – I thought we had to use rec for this, but reading back the documentation, rec is advised when let expression would create too much clutter, and let can indeed reference to self. Thanks for your help.

the rec you’re thinking of is within an attr set (think dictionary). It allows you to refer to other attrs present in the attr set, the most common example being the old name convention:

stdenv.mkDerivation rec {
  name = "hello-{version}";
  version = "1.1.0";

still used commonly in today in most sources, but usually in pull sources:

stdenv.mkDerivation rec {
  pname = "teams";
  version = "1.2.00.32451";

  src = fetchurl {
    url = "https://packages.microsoft.com/repos/ms-teams/pool/main/t/teams/teams_${version}_amd64.deb";
    sha256 = "1p053kg5qksr78v2h7cxia5mb9kzgfwm6n99x579vfx48kka1n18";
  };

the version reference from src comes from the outer attr scope.

1 Like

Thanks again @jonringer for your help – I’m still new at NixOS and I’m working at a high level for the moment, even if I sometime have to meddle with some (not so) lower nix level. But that’s fun, and all your inputs are helping me narrowing down my research!

That case was pretty trivial, but what about a more complicated case?

let
  pkgs = nixpkgsFor.${system};
in {
  default = "something something";

  let name = "myname-1"; in {
    ${name} = pkgs.mkShell {
      name = ${name};
      #...other stuff...
    };
  };

  let name = "myname-2"; in {
    ${name} = pkgs.mkShell {
      name = ${name};
      #...other stuff...
    };
  };
};

This produces an error from the first nested let expression even if it’s not the first statement in the in body.
I need more than one line within the outer let statement, but I have two other nested let statements that can’t encompass the whole outer let. While it’s technically possible to combine them by giving the name in each of the nested let statements a unique name instead, it kind of defeats the purpose of what I was trying to do.

EDIT: stripped out some specifics from the example that were accidentally left in after copy-paste

“Resolving” the inner lets would lead to this (and lets ignore that you used the variables wrong):

let
  pkgs = nixpkgsFor.${system};
in {
  default = "something something";

  {
    "myname-1" = pkgs.mkShell {
      name = "myname-1";
      #...other stuff...
    };
  };

  {
    "myname-2" = pkgs.mkShell {
      name = "myname-2";
      #...other stuff...
    };
  };
};

This would not be valid either, as you need an attribute name for those values.

What do you mean “using the variables wrong”?

For the attribute names you had to use quotes, and for the name = ${name} part you also either had to use quotes ("${name}") or no antiquotation (name).

Ah, my mistake, they should have all been quoted.

About “resolving” the lets though, that pretty much defeats the purpose of let. Your answer is basically “don’t do that”, ignoring the very reason let exists in the first place.

Is it simply not possible to ever nest a let inside another let under any condition? That seems very unlikely since nix is supposed to be a functional language and therefore everything must be able to be nested within everything else (“cons” can’t be constructed otherwise).

You do not have a let in a let in your example.

And “nested” lets work fine…

nix-repl> let a = let b = 1; in b; in a
1

You just need to follow the syntactic rules.

You can use a let everywhere where an expression is allowed. Though where you used your 2 “inner” lets, no expression was allowed, but attribute names only. That was what I wanted to show you with “resolving” the lets.

That is much clearer, thank you.

So the let is an attribute set generator in this case, so it will return an attribute set that has to go somewhere. The inner lets aren’t being assigned anywhere so there’s nothing to trigger their (potentially lazy) evaluation. It’s not an issue with them being nested.

Clearly I’ve had too many years of C macro programming for my own good.

No, it doesn’t have anything to do with lazy evaluation.

It is a plain syntax error to use let where an attribute name is expected.

And I am not sure what you mean by “attribute set generator”. A let is an expression that evaluates to a value. not more, not less.