So Nix is fundamentally built around the Nix expression language; which is a programming language. Creating variables is a huge part of programming.
If you want to package apps or just simplify repetitive configuration files; you will probably need variables.
The let-lets syntax allows you you define a variable that the next expression of code runs in:
A high level example is:
let
x = 1;
y = 2;
in
x + y
This is valid nix code; we can actually run it, if you save it to test.nix
:
> nix-instantiate --eval test.nix
3
We can see here that the code still evaluates (aka. returns) the value 3. This means that anywhere in our code, we can replace some expression
with something like let ... in expression
.
Here is a concrete example of that replacement. We could make our last example more complex by replacing the number 1 with a let-in expression:
let
x = (let a = 2; in a+3);
y = 2;
in
x + y
Which changes the answer:
> nix-instantiate --eval test.nix
7
So we can formalize the let-in syntax as:
let
name = expression;
name = expression;
name = expression;
...
in
expression
Say we have an environment (something we run as nix-shell test.nix
); and it uses a lot of python packages:
with import <nixpkgs> {};
stdenv.mkDerivation rec {
name = "python-environment";
buildInputs = [
pkgs.python36
pkgs.python36.pkgs.flask
pkgs.python36.pkgs.itsdangerous
pkgs.python36.pkgs.six
];
}
Obviously that looks very repetitive; and we repeat the python version many times.
We can refactor this to store the pkgs.python36
as a variable. This makes the code less repetitive. It also makes it easier to change the python version later. The code would look like:
with import <nixpkgs> {};
stdenv.mkDerivation rec {
name = "python-environment";
buildInputs = let
py = pkgs.python36;
in [
py
py.pkgs.flask
py.pkgs.itsdangerous
py.pkgs.six
];
}
Yay! Now we've created an identical environment with less words
Smart cookies reading along would have noticed that we could have put the let-in expression in a different place. For example:
with import <nixpkgs> {};
let
# Assigning the variable `py` to the python we want to use
py = pkgs.python36;
# You could try and change it to python27 to see what happens
in
stdenv.mkDerivation rec {
name = "python-environment";
buildInputs = [
# here we reference `py` rather than `pkgs.python36`
py
py.pkgs.flask
py.pkgs.itsdangerous
py.pkgs.six
];
}
The result of that code would have been identical.
However, putting the let-in expression in a different place changes the scope (or parts of the code) that the py
variable is useable for.
So with the larger scope, we could do something like:
with import <nixpkgs> {};
let
py = pkgs.python36;
in
stdenv.mkDerivation rec {
name = "python-environment";
buildInputs = [
py
py.pkgs.flask
py.pkgs.itsdangerous
py.pkgs.six
];
# The ${...} syntax is string interpolation in Nix
shellHook = ''
echo "using python: ${py.name}"
'';
}
Which could be pretty cool:
sam@vcs ~> nix-shell test.nix
using python: python3-3.6.4
[nix-shell:~]$
However, we couldn't use the py
variable in shellHook
if the let-in expression only covers the buildInputs
list.
# THIS CODE WILL CRASH
with import <nixpkgs> {};
stdenv.mkDerivation rec {
name = "python-environment";
buildInputs = let
py = pkgs.python36;
in [
# `py` is now in scope
py
py.pkgs.flask
py.pkgs.itsdangerous
py.pkgs.six
];
# `py` is now out of scope (a list is one type of "expression")
shellHook = ''
echo "using python: ${py.name}"
'';
}
It would result in a crash:
> nix-shell test.nix
error: undefined variable ‘py’ at /home/sam/test.nix:20:27
With is another expression (like let-in). It has the syntax:
with expression1; expression2
With actually works very similar to the JavaScript with, and nothing like the python with. Basically, it:
ret1
. ret1
must be a set (aka. dictionary)ret1
, and makes them variables in the scope of expression2So you could replace:
let
x = 1;
y = 2;
in
x + y
With the with
equivalent:
with { x = 1; y = 2; }; x + y
This is really useful when dealing with sets that have loads of attributes, like python36.pkgs
. So our old code:
buildInputs = [
pkgs.python36
pkgs.python36.pkgs.flask
pkgs.python36.pkgs.itsdangerous
pkgs.python36.pkgs.six
];
Could become shorter using with
:
buildInputs = with pkgs.python36.pkgs [
pkgs.python36
flask
itsdangerous
six
];
As you can see, all the attributes of pkgs.python36.pkgs
(including flask
, itsdangerous
and six
) were added to the scope when evaluating the list.
This can also be chained with the let-in expression:
buildInputs =
let
py = pkgs.python36;
in
with py.pkgs;
[
py
flask
itsdangerous
six
];
Follow the series on GitHub
Hero image from nix-artwork by Luca BrunoI hope you enjoyed this article. Contact me if you have any thoughts or questions.
© 2015—2024 Sam Parkinson