Okay! Chapter 15 is finally going to teach us the language we’ve been using for the past several chapters. I’m excited.
The Nix expression language is a pure, lazy, functional language.
I didn’t know it was a pure language.
It would be really weird to have a general purpose language with non-strict evaluation that does not explicitly separate side effects from pure code. But this is just a language for describing packages – I could imagine relying on the fact that we don’t evaluate packages until we need to as a way to, like, delay the side effect of installing them (or whatever) until it’s necessary. This was, briefly, how I guessed that fetchurl
worked.
Its main job is to describe packages, compositions of packages, and the variability within packages.
I once again wonder what “compositions of packages” means. And “the variability within packages.” That’s a very poetic sentiment, isn’t it? I enjoy the sound of it; I spend a moment meditating on the fickle nature of packages. But I don’t know… what it means.
15.1. Values
We finally see some types! Yes. I am excited.
Here’s what we’re working with:
- strings
- numbers
- paths
- booleans
- null
Null? Nix appears to be dynamically typed, so I’ll allow it. But consider this your first warning.
There’s like three pages of words about strings here. Normal escape characters. Interpolation (Nix calls it “antiquotation”) with ${...}
, ala shell.
I learn that strings, paths, and derivations can all be “coerced into a string” in an interpolation. Wait, derivations? That wasn’t a type. Or it wasn’t a “basic type?” Hmm. Did the manual just give us a non-exhaustive list of types? Yes. It did.
I’m just gonna leave this one right here:
Since
${
and''
have special meaning in indented strings, you need a way to quote them.$
can be escaped by prefixing it with''
(that is, two single quotes), i.e.,''$
.''
can be escaped by prefixing it with'
, i.e.,'''
.$
removes any special meaning from the following$
.
Any questions?
I learn that URIs can be written as unquoted strings, for some reason. But unlike paths, which actually are their own type, this is just a different string literal syntax. Weird.
Next up: numbers are numbers. Ints and floats. Nothing about the precision of them.
And then paths. Relative file paths are “made absolute at parse time.” Okay? That seems… weird. So if I write:
mkDerivation {
builder = ./builder.sh;
}
Then builder
is going to be /Users/ian/src/nixpkgs/pkgs/whatever/my-thing/builder.sh
? I mean I guess I just need to make sure that paths never leak out into my output or something. Yeah, okay, this makes sense.
Aha:
Paths can also be specified between angle brackets, e.g.
<nixpkgs>
. This means that the directories listed in the environment variableNIX_PATH
will be searched for the given file or directory name.
Okay. Good; I’ve been wondering about <nixpkgs>
.
$ echo $NIX_PATH
/Users/ian/.nix-defexpr/channels
Gotcha. So <nixpkgs>
is short for /Users/ian/.nix-defexpr/channels/nixpkgs
(at the moment). Neat.
Not much to say about bools or nulls.
Then lists… our first type that is not a “basic type.” Lists are lazy in values, strict in length. Alright. I mentally picture a linked list of thunks, not a cons cell pointing to a thunk.1 List elements are just separated by whitespace, not commas. That’s… weird to me. But sure, okay. More parens are never a bad thing in my book.
And finally we come to sets.
I don’t like the word “set.” Does anyone like the word “set,” for this concept? Is anyone okay with this?
I will try to think of the term as shorthand for “sets of bindings,” because that seems to be what they are.
Sets are really the core of the language, since ultimately the Nix language is all about creating derivations, which are really just sets of attributes to be passed to build scripts.
Hmm. Is that really what a derivation is? I feel like a hunter, gently circling the definition of “derivation,” trying not to startle it. One day I will have it in my sights.
We’ve seen plenty of sets by now, so most of the examples are not interesting.
I learn the or
syntax, which is weird:
{ a = "Foo"; b = "Bar"; }.c or "Xyzzy"
The documentation implies this is a language construct, not a null coalescing operator or something – or
must be attached to an attribute access.
I learn that attribute names can be arbitrary strings, and can be quoted, both in a declaration or a lookup. Even dynamically determined:
{ "foo ${bar}" = 123; "nix-1.0" = 456; }."foo ${bar}"
I learn the equivalent of JavaScript’s foo[bar]
syntax is foo.${bar}
: you can omit the quotes in this simple case.
In the special case where an attribute name inside of a set declaration evaluates to
null
(which is normally an error, asnull
is not antiquotable), that attribute is simply not added to the set.
Neat.
And then we take a sharp turn:
A set that has a
__functor
attribute whose value is callable (i.e. is itself a function or a set with a__functor
attribute whose value is callable) can be applied as if it were a function, with the set itself passed in first , e.g.,let add = { __functor = self: x: x + self.x; }; inc = add // { x = 1; }; in inc 1
Okaaaay. Don’t know what //
is, but this makes sense. We have “callable” sets.
Wait, what is //
? Partial application, by name, to a curried function? No; that’d be insane. x
means two things here. I guess it’s “set union”? So inc
ends up looking like:
{
__functor = self: x: x + self.x;
x = 1;
}
Sure. Okay. That makes sense. Crystal clear example.
This can be used to attach metadata to a function without the caller needing to treat it specially, or to implement a form of object-oriented programming, for example.
You take that back. I thought this was a safe space.
15.2. Language Constructs
Recursive sets, which we’ve seen.
let
expressions, which probably look super weird to most people but which I am familiar with from Haskell and OCaml. If I were writing the manual I would call out the difference between “variables” and “bindings” here and explain how sequences of statements don’t make sense in a purely expression-oriented world and thus we need to scope bindings over another expression using in
instead of just “setting a variable.”
But I am not writing the manual. Good thing, probably.
I learn that in addition to inherit
I can inherit from another set.
Once upon a time someone paid me to write something called “CoffeeScript,” which uses this syntax for declaring objects:
{ foo: bar, baz }
That’s short for:
{ foo: bar, baz: baz }
And I wished, on more than one occasion, that I could also write this:
{ foo: bar, something.baz }
As shorthand for:
{ foo: bar, baz = something.baz }
But I could not. At least not at the time. This was years and years ago. Maybe they added that? I assume CoffeeScript does not exist anymore, now that ES6 is widely available. To retain my innocence, I do not google it.
But in Nix, it seems, I can finally live the dream:
{ foo = bar; inherit (something) baz; }
Ummm. Yes. Very… elegant. I think I’m good.
Functions. Function parameters are patterns, which I am familiar with, but it seems that I cannot use lists as a pattern – only sets. I’m fine with that. I learn that I can use ...
to ignore extra attributes in a set:
{ a, b, c, ... }
Optional attributes in set arguments. Alias patterns. Caveat that the alias patterns do not include the defaulted values. I’m very glad they call this out because I could see that going either way. Thank you, Nix manual author.
Conditional expressions: if foo then bar else baz
. It’s sort of interesting to think about the fact that lazily evaluated languages don’t need an explicit built-in language feature for branching – you can get away with a built-in function if(foo, bar, baz)
, and only one expression will actually end up being evaluated. But I find the if-then-else
syntax much easier to read.
Assertion expressions!
assert e1; e2
This is another thing that might look weird coming from languages with statements – this is still an expression, even though it looks like that semicolon is acting as a statement terminator. Don’t be fooled.
Example 15.1. Nix expression for Subversion
The manual walks through an example of a “real” package:
{ localServer ? false , httpServer ? false , sslSupport ? false , pythonBindings ? false , javaSwigBindings ? false , javahlBindings ? false , stdenv, fetchurl , openssl ? null, httpd ? null, db4 ? null, expat, swig ? null, j2sdk ? null }: assert localServer -> db4 != null; assert httpServer -> httpd != null && httpd.expat == expat; assert sslSupport -> openssl != null && (httpServer -> httpd.openssl == openssl); assert pythonBindings -> swig != null && swig.pythonSupport; assert javaSwigBindings -> swig != null && swig.javaSupport; assert javahlBindings -> j2sdk != null; stdenv.mkDerivation { name = "subversion-1.1.1"; ... openssl = if sslSupport then openssl else null; ... }
It’s very straightforward, having already seen the hello
example, so I won’t really say many words about it. Except to highlight this line:
assert sslSupport -> openssl != null && (httpServer -> httpd.openssl == openssl);
I assume that this is an implication operator, even though I don’t think I have ever seen a language with an implication operator before. That’s very nifty: people think in terms of implication all the time, but usually have to write the gross expansion using negation and disjunction. Yeah, that’s right. I know some words.
Moving past the example, I learn about with
expressions – basically an equivalent of open
or use
or whatever. Do you remember when JavaScript had with
? Man, what a crazy time to be alive. I assume it’s still in there, technically, because getting rid of it would break… I don’t know. Something that deserves to be broken. Anyway, it probably makes sense in Nix.
Shoutout to some documentation I love:
The most common use of
with
is in conjunction with theimport
function. E.g.,with (import ./definitions.nix); ...
Thank you. I love that. Show me a thing, tell me how it works in a vacuum, then tell me how it’s typically used. Make it concrete.
This is very unexpected to me:
The bindings introduced by
with
do not shadow bindings introduced by other means.
So basically let x = 1 in with { x = 2 }; x
evaluates to 1
. I understand why Nix wants to do this – preventing action at a distance significantly changing the meaning of your code – but it’s weird. It doesn’t feel principled – it breaks my mental model of “linked list of scopes searched upwards” that I am used to from every other language with lexical scope. It’s like two linked lists now, I guess, and we search the “primary” list of scopes – let
bindings and function names and whatever – before we look in the “fallback” scopes, which are extended by with
? I mean, okay. Weird, but pragmatic.
Hybrid comment style: we have #
and /* ... */
. I like that, personally, but it’s a little weird.
15.3. Operators
This is a big long table of stuff; I won’t repeat it all here. Mostly stuff you’d expect. Some new stuff:
set ? attr
is like JavaScript’s of
operator; it checks if an attribute is defined on a given set. ++
concatenates lists, like Haskell – apparently there’s no operator overloading, so we can’t use +
? Except that, no, +
is used to concatenate strings. So that’s weird.
I called //
“set union;” they call it “set update” – I had the same semantics in mind, but of course these aren’t sets they’re actually maps so we need to do something on key collision – and it picks the righthand values, as you’d expect.
No other operators over maps– excuse me, “sets.”
That’s the end of this section, although the chapter is still going. But the next section is called “Derivations,” and I assume that it will deserve to get its own blog post, so I’ll end this one here.
- What is the “type” of a derivation?
- What are the sizes of integral and floating point numbers?
-
I picture thunks as little cartoon thought bubbles and now maybe you do too. ↩︎