You may want to inform yourself about human rights in China.

On Nix's Language: Introduction

tags: computing nix
date: 2022-11-03
update: 2024-04-03

NixOS is, to my surprise, an already 20+ years old Linux distribution (as of 2023), based on the Nix package manager, itself based on the Nix expression language, which will be the focus of this article and this series (I won’t talk about NixOS, nor about the Nix package manager, neither about how to use Nix’s language to interact with Nix package manager).

Nix’s language is:

It “feels” either like a lazy Scheme (tuples, dynamic typing), or a stripped-down Haskell (laziness, not a S-expression based syntax).

The goal of the current article is to introduce elementary technical elements:

We’ll later use this knowledge to explore recursive functions in the next two articles, and show how much we can achieve with mere recursive functions. We’ll then shift our focus on Nix’s lists & sets, before concluding with two project-based articles, the last one being about implementing a simple λ-calculus interpreter, which is the main goal of this series.

All the code is available on github.

Note: Thanks Pete Dietl for the various suggestions!


Backwoods (Лесная глушь, 1870), ink on toned paper (?)

Backwoods (Лесная глушь, 1870), ink on toned paper (?) by Ivan Ivanovich Shishkin (Ива́н Ива́нович Ши́шкин, 1832-1898) through wikiart.orgPublic domain

Installation

We don’t need to install NixOS to use the Nix-the-language; one still need to install the Nix package manager, but it will live happily without much interference, on any Linux distribution. You may want to refer to the official quick start, or to Archlinux’s wiki Nix page (even if you don’t use Archlinux, you may find interesting bits for your distribution there):

(root)% yes | pacman -S nix
...

If you don’t care much for the package manager itself, you can perform a single-user installation, which implies chown(8)-ing /nix:

(user)% nix repl
error: creating directory '/nix/store': Permission denied
(root)% mkdir -p /nix/; chown -R $user:$group /nix/

Executing code

There’s a REPL available (corresponding manual page):

% nix repl
Welcome to Nix 2.11.1. Type :? for help.

nix-repl> let a = 3; b = 2; in a * b
6
nix-repl> ^D

Code can also be executed from a file:

% cat > /tmp/t.nix
let a = 3; b = 2; in a * b
^D
% nix-instantiate --eval /tmp/t.nix
6

The command can be used as a shebang:

% cat > /tmp/t.nix
#!/usr/bin/env -S nix-instantiate --eval
let a = 3; b = 2; in a * b
^D
% chmod +x /tmp/t.nix
% /tmp/t.nix
6

Hello, World

Sketching

Let’s demonstrate a bunch of features within a single script, using variants of a hello world. First, note that Nix doesn’t come a regular print-like command, nor does it have statements. Furthermore, each script must evaluate to a single value. Let’s see what does it mean (I’ll progressively revisit in greater details what’s presented here throughout the series, so you may have to use your intuition for now).

Consider for instance the most basic hello world, which simply evaluates to the string "hello world":

#!/usr/bin/env -S nix-instantiate --eval
"hello world"
"hello world"

Now if we want to add a second variant to the same script, say, to exemplify string concatenation: Nix interprets it as a function call (where "hello world" is the function and "hello"+" "+"world" an argument):

#!/usr/bin/env -S nix-instantiate --eval
"hello world"
"hello"+" "+"world"
error: attempt to call something which is not a function but a string: "hello world"
       at ./nix/hw/hw1.nix:2:1:
            1| #!/usr/bin/env -S nix-instantiate --eval
            2| "hello world"
             | ^
            3| "hello"+" "+"world"

You may be tempted to use a semicolon ; to separate the two values, but as earlier stated, Nix doesn’t have (imperative) statements:

#!/usr/bin/env -S nix-instantiate --eval
"hello world";
"hello"+" "+"world"
error: syntax error, unexpected ';', expecting end of file
       at ./nix/hw/hw2.nix:2:14:
            1| #!/usr/bin/env -S nix-instantiate --eval
            2| "hello world";
             |              ^
            3| "hello"+" "+"world"

Well, you may ask, what about returning a list? After glancing at the documentation, you’d learn that lists are written within brackets [], its elements being whitespace-separated:

#!/usr/bin/env -S nix-instantiate --eval
["hello world"  "hello"+" "+"world"]
error: syntax error, unexpected '+'
       at ./nix/hw/hw3.nix:2:24:
            1| #!/usr/bin/env -S nix-instantiate --eval
            2| ["hello world"  "hello"+" "+"world"]
             |                        ^
            3|

Almost there, we just need to wrap our expressions between parentheses (if you think about it, it’s a natural consequence of the fact that lists are whitespace-separated), but something unexpected’s happening:

#!/usr/bin/env -S nix-instantiate --eval
["hello world"  ("hello"+" "+"world")]
[ "hello world" <CODE> ]

If you’re familiar with laziness, you might have (correctly) guessed that the concatenation hasn’t been evaluated. Can we find ways to force the evaluation?

There are some builtin ways to convert a list to a string, which as a side effect, will force the evaluation of a list’s elements; this also demands us to learn how to define a variable:

#!/usr/bin/env -S nix-instantiate --eval
let
	xs = [ "hello world" ("hello"+" "+"world") ];
in
	"${toString xs}"
"hello world hello world"

Exercise: The previous example exemplifies [a superfluous use of] string interpolation: try to simplify it

Solution:[+]

An alternative to force the evaluation of a variable, is to use builtins.deepSeq e1 e2, which evaluates e1 before returning e2. The evaluation is performed, well, deeply: if e1 is an intricate data-structure (e.g. a list, or a list of lists), then it will be recursively evaluated. This means that builtins.deepSeq x x will first force a complete evaluation of x, and then return x, hence the following:

#!/usr/bin/env -S nix-instantiate --eval
let
	xs = [ "hello world" ("hello"+" "+"world") ];
in
	builtins.deepSeq xs xs

[ "hello world" "hello world" ]

The problem with those two last approaches is that they force us to know more about the language that I’d want us to for now (we’ll purposely avoid using Nix’s lists for a while). Instead, let’s look at one more solution: while Nix does not provide a regular print-like, it does provide a builtins.trace e1 e2 function, which:

  1. prints e1 (to stderr) and then;
  2. evaluates e2.

This means that we can easily chain tracing (and inject tracing in statement-free code):

#!/usr/bin/env -S nix-instantiate --eval
builtins.trace(
	"hello world"
) builtins.trace (
	"hello"+" "+"world"
) "EOF"
trace: hello world
trace: hello world
"EOF"

Note: We could have taken a different approach, for instance by defining an alternative function to toString to “stringify” a list, but we’ll explore using regular Nix lists in the third article of this series only. Getting to know builtins.trace early on seems to be a wise choice anyway: you now know how to debug Nix programs.

Note: The REPL comes with some tools to help with some of those issues; you may want to refer to its documentation for more as this won’t be covered here.

Examples

Finally, here’s an executable Nix file which executes variants of a hello world, progressively introducing you to various features of the language:

#!/usr/bin/env -S nix-instantiate --eval
/*
 * Regarding ``with builtins;``, it is an import-like.
 * From a user perspective, suffice to know, for now,
 * that it means we won't have to prefix "trace" with "bulitins"
 * everywhere.
 *
 * More on this later.
 *
 * Mind the two kinds of comments used throughout this file.
 */
with builtins;

# most basic
trace ("hello world")

# string concatenation
trace ("hello"+" "+"world")

# "variables" definition and usage
# NOTE: mind the semicolon after each variable
# declaration.
trace (
	let
		h = "hello";
		w = "world";
	in
		h+" "+w
)

# strings "format" (w is a list containing a single
# element)
trace (
	let
		h = "hello";
		w = ["world"];
	in
		"${toString h} ${toString w}"
)

# Same as before, but with a single list with two
# elements
trace (
	let
		hw = ["hello" "world"];
	in
		"${toString hw}"
)

# lambda function, list of argument (one arg)
trace (
	(x: "hello "+x) "world"
)

# named function, list of argument (two args)
trace (
	let
		f = x: y: x+" "+y;
	in
		f "hello" "world"
)

# named function, "named parameters" (attribute sets)
trace (
	let
		hw = {h, w} : "${h} ${w}";
	in
		hw {h="hello"; w="world";}
)

# default parameters
trace (
	let
		hw = {h ? "hello", w ? "world"} : "${h} ${w}";
	in
		hw {h="hello";}
)

# if/then/else
trace (
	let
		hw = x: if x == "hello" then "hello world" else "bye";
	in
		hw "hello"
)

# Evaluation value for this script
"OK"

trace: hello world
trace: hello world
trace: hello world
trace: hello world
trace: hello world
trace: hello world
trace: hello world
trace: hello world
trace: hello world
trace: hello world
"OK"

Note: I’m voluntarily exploring the language partially for now; we’ll slowly be more thorough as we progress. Just make sure you get an intuitive understanding of what’s happening.

Exercise: Try to re-implement the previous program by returning deepSeq on a list containing each of the variants.

Solution:[+]

Exercise: Try to implement a few new variants of the hello world program.

Solution:[+]

Resources

If you want to dive further, have a look at (in no specific order):

The next article will focus on recursive functions: this will thus be an introductory course on functional programming.

Backwoods (Сумерки, 1883), oil on canvas

Backwoods (Сумерки, 1883), oil on canvas by Ivan Ivanovich Shishkin (Ива́н Ива́нович Ши́шкин, 1832-1898) through arthive.com wikiart.orgPublic domain


In the series:


Comments

By email, at mathieu.bivert chez:

email