Nix by example

From NixOS Wiki
Revision as of 02:51, 3 October 2020 by imported>AndersonTorres (Work In Progress)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Part 1: The Nix expression language

Nix is a package manager. ‘Nix’ is also the name of the programming language that it uses. The language can actually be used independently, without any package management at all. Here I show the Nix expression language by example. My approach is to introduce expanding subsets of the language: at any point you can stop reading and have a full understanding of the subset introduced up to that point.

I’ll assume you’ve already installed Nix. Next stop, hello world! From the terminal, run:

 $ nix-instantiate --eval --expr '"Hello world"'
 "Hello world"

What happened here? Nix took the expression "Hello world", evaluated it, and printed out the value. This means that the following is a valid Nix expression:

 "Hello world"

and, since it is already a value, the result of evaluating it is exactly the same string:

 "Hello world"

Notice that we did not tell Nix to print the string. Explicit output is not part of the language. In fact, there are no commands at all. You cannot write to a file or call an external command. There is no way to read input. The only thing that happens is evaluation of an expression to a value. Think lambda calculus, not Java.

The expression language introduced so far is extremely simple:

 Expression ::= String
 String     ::= '"' StringChar* '"'
 StringChar ::= Space | Alphanumeric

Notice strings are enclosed in double-quote characters! Using single-quotes will give you an error:

 $ nix-instantiate --eval --expr "'Hello world'"
 error: syntax error, unexpected $undefined, at (string):1:1

Special characters

So far, the string expressions we’ve seen evaluate to a list of alphanumeric characters and spaces. But in general, a string is a list of unicode code points. We must learn how to write those code points in string expressions.

For example, I haven’t yet taught you how to write a literal double-quote in a string. Using a plain double-quote will result in a confusing error:

 > "He said "Hello world""
 error: undefined variable `Hello' at (string):1:11

Instead, we use the backslash as the escape character.

 > "He said \"Hello world\""
 "He said \"Hello world\""

Notice Nix uses the same encoding in its output. To encode a literal backslash we use two backslashes:

 > "Write \\\" to write a literal double-quote"
 "Write \\\" to write a literal double-quote"

Thus our grammar expands to:

 StringChar ::= '\\' | '\"' | Space | Alphanumeric

The backslash is used to encode many characters:

Sequence Encodes character Unicode code point
\n line feed 10
\t tab 11
\r carriage return 13
\\ backslash 92

Primitive types and operators

The expression we gave Nix was really just a value — in particular, a string. Nix knows about other standard kinds of values:

 $ nix-instantiate --eval --expr '42'    # integers
 42
 $ nix-instantiate --eval --expr 'true'  # booleans
 true

The only primitive numeric type is the integer. Nix has no ‘floating point’ or rational types — apparently, package management just doesn’t call for anything except integers.

Nix also has familiar operators to manipulate those values. Here are some examples, which are also our first examples of real evaluation:

 $ nix-instantiate --eval ––expr '"Hello " + "world"'
 "Hello world"
 $ nix-instantiate --eval ––expr '2 + 3'
 5

Standard stuff: + is a binary operator which concatenates strings or adds integers. By this point, we can use Nix as a simple desk calculator:

 $ nix-instantiate --eval ––expr '(400 + 2) * (-5) + (5 * 30)'
 -1860
 $ nix-instantiate --eval --expr '(4 * 4 * 4) < (5 * 5 * 5)'
 true

Addition is easy; let’s try division:

 $ nix-instantiate --eval --expr '2/3'
 /Users/jhf/dev/nix/2/3

Hmm. Not what you expected? That weird answer is because Nix interprets the expression as a path, and I’ll explain that later. For now, just get used to being liberal with whitespace:

 $ nix-instantiate --eval --expr '2 / 3'
 0

This division operator sure is tricksy! Remember I said Nix only has integers? Because integers are not closed under division, Nix has to do integer division (rounding towards zero).

REPL

From now on, I’ll omit the nix-instantiate command and pretend we’re in a REPL, like this:

 > (4 * 4 * 4) < (5 * 5 * 5)
 true

If you want a real REPL, you can install nix-repl:

 $ nix-env -i nix-repl
 $ nix-repl
 nix-repl> 4*4*4 < 5*5*5
 true

Errors

We saw that the + operator works on both strings and integers. Hmm … can we mix strings and integers?

 > "Hello" + 6
 error: cannot coerce an integer to a string, at (string):1:1

… nope. Unlike some crazy languages, Nix correctly tells us that a string added to an integer is a stupid. Sharp-eyed readers will notice I lied earlier when I said that the only thing that happens is ‘evaluation to a value’. Actually, one of two things happen: either evaluation terminates with a value, or aborts with an error. An error signals a mistake by the programmer, and there are several kinds of mistakes we’ll see which result in such errors. You can also generate errors explicitly with the abort builtin:

 > abort "Just not feeling it today"
 error: evaluation aborted with the following error message: `Just not feeling it today'

Errors are fatal: if anything evaluates to an error, the entire program stops. You cannot ‘catch’ an error. (Although we will see later that Nix also has exceptions, which can be caught.)

Typing discipline

When we made the mistake of adding a string to an integer, the type error that Nix gave us was a runtime type error, not a compile-time type error. We did not annotate our expression with a type declaration, and Nix did not attempt to infer a type for it before doing evaluation. Nix has no type-checking phase (though there are plans for static typing) or any user-facing notion of compilation; it jumps straight to evaluation. You can call it dynamic, if you like.

Nix is able to give us sensible runtime type errors like this because values are tagged with their type, as in many other scripting languages. Before performing the + operation, Nix checks the tags of the values. If they are both strings, it concatenates them; if they are both integers, it adds them; else it aborts with an error.

Nix also exposes these tags to the program via the typeOf builtin:

 > builtins.typeOf "foo"
 "string"
 > builtins.typeOf (2 + 2)
 "int"
 > builtins.typeOf ("foo" + 2)
 error: cannot coerce an integer to a string, at (string):1:18

And for each type T, there is also a convenience isT builtin:

 > builtins.isInt (2 + 2)
 true
 > builtins.isBool "true"
 false
 > builtins.isBool false
 true

Even though Nix is dynamically typed, it can help to describe expressions with types. Where it helps, I’ll write annotations like:

 # 6 : int
 # builtins.isInt : any -> bool
 # builtins.isInt 6 : bool
 # builtins.typeOf : any -> string

Function application

You have seen some builtin functions such as abort and typeOf. The syntax for function application simply uses whitespace. The argument does not require parentheses around it, though it doesn’t hurt:

 > builtins.isInt 4
 true
 > builtins.isInt(4)
 true

All the functions we have seen take a single argument. Now let’s look at one that takes two arguments. Integer division is available as a builtin function:

 > builtins.div 10 5
 2

We write the function name followed by all the arguments in order, all separated by whitespace. Parentheses are not necessary, but they can help to show how the expression is parsed:

 > (builtins.div 10) 5
 2

If this doesn’t look familiar, read it carefully. I lied when I said that div ‘takes two arguments’. Actually, functions in Nix always take exactly one argument, and multi-argument behavior is achieved via currying. That means that builtins.div is a function which takes an int, and returns another function, which in turn takes another int and finally returns the value.

This means that we don’t have to provide all the arguments at once:

 > builtins.typeOf   (builtins.div)
 "lambda"
 > builtins.typeOf  ((builtins.div) 10)
 "lambda"
 > builtins.typeOf (((builtins.div) 10) 5)
 "int"

Just like in lambda calculus, functions in Nix are first-class values. The expression ‘builtins.div 10’ gives us a first-class function value, which we can pass around in our program in the same way we pass around integers and strings. Following ML, I would write the static type of builtins.div like this:

 # builtins.div : int -> int -> int

You should mentally parse the -> as right associative, i.e.:

 # builtins.div : int -> (int -> int)

Function definition

We can define our own functions by writing anonymous function literals. The syntax is slightly unusual, though. Let’s write a function to square an integer:

 > x: x*x     # int -> int
 <LAMBDA>

This is the same as (λx. x*x) in lambda notation, or function(x){return x*x;} in JavaScript. The x before the colon is the name of the bound variable, and the x*x after the colon is the returned expression. Our function value is just like the builtin functions:

 > builtins.typeOf (x: x*x)
 "lambda"

We can apply an integer argument to our function to get its square:

 > (x: x*x) 3
 9

(By the way, since we now have abstraction and application, we have the lambda calculus, and so Nix is clearly Turing-equivalent.)

Earlier I said that functions of multiple parameters can be defined via currying. Let’s define a function which takes two integers and returns the sum of their squares. This does not actually require any new syntax:

 > (x: y: x*x + y*y)   # int -> int -> int
 <LAMBDA>
 > (x: y: x*x + y*y) 3 7
 58

Originally taken from: https://medium.com/@MrJamesFisher/nix-by-example-a0063a1a4c55 Archive: http://archive.is/uPUC7