Nix comes with the powerful, functional Nix (programming) language to manage packages of many ecosystems at once; not just Javascript or Java like Node and Maven do. It’s like writing a declarative package.json or pom.xml file but on steroids: Besides dependency management we get flexible, reproducible development environments for free.

The goal of the Nix language is not to be a general purpose programming language like Haskell or Scala but to make it easy to write a declarative package definition (called a “derivation” in the Nix community) for any programming language and any other app or tool.

A raw Nix package definition looks a lot like Json and roughly as follows:

{
  output = "myProject-1.0.0" # app name, version
  inputDrvs = ["bash" "nodejs", "zlib"] # Dependencies
  inputSrcs = ["buildScript.sh"]        # how to build package
  ...
}

This example declares that in order to build the package myProject we require a Bash shell environment, a Node Javascript interpreter and a special C library. In order to build that package, we also need to run a build script. We can mix many ecosystems as depenencies for our package!

Such a package definition is declarative, but in its full form actually quite long, because there is a lot to configure to support anything as a package; much of which is boilerplate. Therefore a definition usually written with functions and clever patterns to make it as short as possible while still being flexible and customizable. In practice it looks more like this:

{ pkgs ? import <nixpkgs> {} }:
pkgs.stdenv.mkDerivation {
  name = "myProject-1.0.0";
  src = ./src; 
  buildInputs = [ pkgs.nodejs ];
}

It uses the great power of a functional programming language: The Nix language. This syntax might look weird at first, but is similar to other functional programming languages like Haskell. Don’t worry if you haven’t programmed in such a language before: In this post we will learn how to read it from a Object-oriented programmer perspective.

Nix (Json-like) Attribute Sets

The most important concept in Nix is called an “attribute set” but you can think of it as Javascript object with key-value pairs:

# package definition
{
  name = "myProject-1.0.0";
  src = ./src; 
  buildInputs = [];
  buildPhase = ''
    ./myBuildScript.sh
  '';
  ...
}

A Nix package definition (called a “derivation” or “build action” in Nix) is just a regular attribute set with some agreed upon attribute names. Those attributes define all depenencies and necessary (build) steps to compile that package and use it as library or an app.

info

Each key-value pair is an “attribute”, so we have a set of attributes. Hence the name “attribute set”. As with Object-Oriented programming languages like Java or Javascript, you can use the dot notation to access an attribute:

let
  package = {
      name = "myProject-1.0.0";
      ...
    }
in
package.name

Here, package.name evaluates to "myProject-1.0.0".
More: Learn about let

Some of the few other types are: Strings like "myProject-1.0.0" and ''./myBuildScript.sh'', paths like ./src, and lists like [].
More: A summary all important Nix language constructs
More: The full Nix language documentation

So far we don’t have any real benefit compared to Json, just yet another syntax variation like Yaml or HJson.

Functions

Within Nix the real power comes from having functions; not just ordinary functions like with Bash scripting but mathematically pure, side-effect free, real functional-programming-style functions similar to Haskell.

Calling Functions

One of the most basic functions, that is used in almost every Nix package definition, is mkDerivation:

...
stdenv.mkDerivation {
  name = "myProject-1.0.0"; # required
  src = ./src;              # required
  # everything else is optional
}

This function takes a minimal, incomplete attribute set and returns a big, full package definition attribute set (called “derivation” in Nix). In package definitions on the web you will usually find it written as stdenv.mkDerivation to show that it is part of Nix standard environment library (stdenv is itself an attribute set containing helper functions).
More: Derivations in detail
More: What attributes can you pass to mkDerivation?

info

Function calls in Nix don’t use () parenthesis: f(x) in Java/Javascript is simply f x in Nix. So

# mkDerivation :: AttrSet -> AttrSet
mkDerivation { name = "myProject-1.0.0"; src = ./src; }
#|-function-| |------------argument-value--------------|

is the same as in Javascript:

mkDerivation( {name: "myProject-1.0.0", src: "./src"} )

In functional programming languages you will often find mk.. in function names, because it’s shorter than “make”, “create” or “build”.

The Import Function

Almost everywhere we encounter the following expression:

import <nixpkgs> {}

This import function is one the few built-in functions in the Nix language, meaning that you can use it “out of thin air”. mkDerivation for example is not built-in, so it has to be imported from somewhere: import pathArgument takes a single path argument like <nixpkgs> and literally returns the content of that file.

When we write import <nixpkgs> {}, it appears as if import has two arguments. But Nix interprets that expression as (import <nixpkgs>) {}. So there is actually just one argument to import, namely <nixpkgs>. This is a short Nix-notation for a file that contains Nix code that evaluates to an attribute set with all of the more than 80000 Nix packages. That file comes with the Nix package manager and is usually placed at ~/user/.nix-defexpr/channels/nixpkgs).

To be precise, the file behind <nixpkgs> contains a function that only returns the package list when we pass it a set of options; import <nixpkgs> simply loads this function. Since all the options are optional *ba dum tss*, we usually just pass an empty attribute set {}. So, that’s why we always see import <nixpkgs> {} and not just import <nixpkgs>.
See: <nixpkgs> options

info

import <nixpkgs> {} evaluates to a huge attribute set containing all of more than 80000 Nix packages:

{
  nodejs = pkgs.stdenv.mkDerivation {... zlib ...}
  ...
  git = ....
  ...
  firefox = ...
  ...
  jdk8 = ...
  ...
  zlib = ...
  ...
  stdenv = { mkDerivation = ...; ...}
  ...
}

Each package like nodejs (called a “derivation”) is itself a big (package definition) attribute set. The Node package is roughly created as follows:

...
pkgs.stdenv.mkDerivation {
  name = "nodejs-13.0.0";
  # requires the zlib derivation 
  buildInputs = [ pkgs.zlib ...] 
  ...
}

Notice how it cross-references to the zlib Nix package. If you’re curious, take a look at the actual defintion of the Nodejs Nix package.

Let there be Variables

When you need to use a lot of parenthesis, the dot notation chain becomes long or when you need to repeat the same code at several locations, the code quickly becomes unreadable. For these situations it’s better to use (intention-revealing) variables:

let 
 pkgs = import <nixpkgs> {}; # pkgs is the huge attribute set of Nix packages
 stdenv = pkgs.stdenv;       # stdenv is an attribute set with helper functions 
in
stdenv.mkDerivation {
  name = "myProject-1.0.0";
  src = ./src; 
  buildInputs = [ pkgs.nodejs ];
}

With a let ... in ... expression you can define variables in the let-part that you want to use in the in-part. This gives an expression like import <nixpkgs> {} a meaningful name.

As a rule of thumb: An expression is a piece of code that represents (= evaluates to) a value and that you can assign to a variable like:

pkgs = import <nixpkgs> {}; 
#      |---expression ----|
stdenv = pkgs.stdenv; 
#       |-expression -|
myPackage = stdenv.mkDerivation { ... };
#           |------- expression -------|
seven = let a = 2; b = 5; in a + b;
#       |------- expression -------|

In Nix every language construct is an expression. There are no statements like in Javascript:

const a = for (i = 0; ...) { ... }; // ⚡ doesn't work
//        |---- statement -------|
const a = while (i < 10) { ... };   // ⚡ doesn't work
//        |---- statement -----|
const a = if (i < 10) { ... } else { .. };   // ⚡ doesn't work
//        |--------- statement ---------|

Code that consists of expressions instead of statements is often easier to understand , because you can think less about what that code procedurally does and more about what it declaratively is (=what value it represents).


info

In Nix every variable is immutable, which means that you cannot modify it in any way. If you want to modify a variable, you need create another variable. (see why you should favor immutability).
Combined with side-effect free functions this gives you referential transparency: You can refactor any duplicate code by introducing a variable or vice versa. For example, you can introduce a variable for (import <nixpkgs> {}) in

let 
 stdenv = (import <nixpkgs> {}).stdenv;
 nodejs = (import <nixpkgs> {}).nodejs;
in
...

and turn it into

let 
 pkgs = import <nixpkgs> {}; 
 stdenv = pkgs.stdenv;
 nodejs = pkgs.nodejs;
in
...

without changing the end result. This works for any expression in Nix, but not in Javascript or Java.

Defining Functions

Nix has a concise notation to let us write functions ourselves. For example:

x: x+2

represents the function that takes an argument x and adds two. This function has no name. You probably know this from Javascript as an (anonymous) arrow or lambda function x => x+2 from Javascript. In Nix, every function is defined as a lambda function, although we can give it a name by assigning it to a variable:

plusTwo = x: x+2

Calling plusTwo 3 would then evaluate to 5.

In Nix a function with two arguments is written as:

add = x: y: x+y

This is the same as in Javascript:

// javascript
(x, y) => x+y
// or more traditionally
function add(x, y) {
 return x+y;
}

Calling add 2 3 would then evaluate to 5.

In Nix a function with two arguments is actually a function with a single argument that returns another function which you can give the second argument to get the end result. This is called Currying (after the mathematician Haskell Curry) and allows us to write:

plusTwo = add 2

Only supplying the first argument to add returns a regular function that we can assign to a variable: Calling plusTwo 3 now would still evaluate to 5.


Nix also supports pattern matching (also called destructuring in Javascript). It allows us to deconstruct an object into its pieces and give each piece a name to use it compactly. For example:

greet = {name}: "Hello "+name
# is more readable than 
greet = attrset: "Hello "+(attrset.name)

It defines a function that accepts an attribute set, that must contain a name attribute, and returns a string text. Calling greet {name: "World"} would then evaluate to "Hello World".

info

Nix supports default values on attribute destructuring by writing ? like in

greet = {name ? "Java"}: "Hello "+name

So, calling greet {} (without a name attribute) uses the default value and evaluates to "Hello Java".

Functions with default values are used in almost all Nix configuration. Frequently you will see something similar to:

{ pkgs ? import <nixpkgs> {} }:
pkgs.stdenv.mkDerivation {
  name = "myProject-1.0.0";
  src = ./src; 
  buildInputs = [ pkgs.nodejs ];
}

Instead of defining a derivation directly, we turn it into a function that accepts an attribute set that contains an attribute pkgs (with all Nix packages), and only then returns the derivation. If no such attribute is given, then we use the usual <nixpkgs> packages (with the usual nodejs package) by default. This pattern is useful, because some other project using our package, could give us a Nix package set pkgs where the nodejs package comes in a different version.
More: Guide to Nix Derivations
More: How to use Nix shell
More: A summary all important Nix language constructs
More: The full Nix language documentation
More: Talk on Nix basics
More: Talk on how Atlassian (company behind Jira, Confluence) uses Nix

Tags: nix language java maven nodejs javascript beginner

Malte Neuss

Java Software Engineer by day, Haskell enthusiast by night.

Other Posts In Series

Just Enough Nix For Programming

How to Setup Programming Environments with Nix Shell

With a few lines you can setup programming environments (compilers, packages, databases, browsers, and other tools from different ecosystems) with having only the Nix Package Manager installed.

Read More

Just Enough Nix For Programming

How to Search For Apps and Tools within Nix Packages

Nix is very capable of managing dependencies and creating programming environments. Everything is bundled up as Nix packages. However, when we look for an app in a specific version, it can be difficult to find the right Nix package name.

Read More

Just Enough Nix For Programming

Declarative App Builds and Environments: How to create Nix Derivations

Derivations are recipes to build and distribute apps of any ecosystem in Nix. They are similar to Docker files but better, because the Nix programming language allows custom functions like mkDerivation and mkShell to hide complex details.

Read More