loading...

A Web Developer Walks into a Nix expression...

scotttrinh profile image Scott Trinh ・3 min read

I recently needed to reinstall macOS on my work laptop, and right at the point I was going to install Homebrew for the millionth time in my career as a web developer, I paused. I've been following the Nix world with some interest over the past year or so, and I thought it might be a fun weekend experiment to see if I could get my development environment setup by just using Nix as my package manager!

What is Nix?

Nix is really a programming language, with certain special features you'd want for building an OS and package manager, which was certainly surprising to me! I was expecting something more declarative, but it turns out, you really do want a full programming language when building packages. It's a bit like the Grunt vs. Gulp debate of the earlier 2010's, if that is a reference that makes sense.

Why Nix instead of Homebrew for macOS?

My reasoning was two-fold:

  1. The idea of using the same package manager for my work laptop (macOS) and home laptop (Ubuntu) was appealing.
  2. Nix has a feature that rivals Docker for having per-project environments, but without the runtime overhead of the full Docker VM. I wanted to leverage that, and figured one way of getting "better" at Nix was to use it everywhere. At work, we pin our version of Node, but I wanted to be able to easily move back and forth between Node versions (more on this later).

What is Nix like?

Nix is a purely-functional, strongly-typed language. It isn't statically-typed, but you have to do explicit casting. What does that mean? Consider the differences between JavaScript (dynamically-typed) and TypeScript (statically-typed):

JavaScript

let foo = true;
foo = 1;
foo = 'true';
// This is all fine

TypeScript

let foo: boolean = true;
foo = 1; // Type error!

Nix

let
  foo = true;
  foo = 1; // Type error!

So, Nix is a bit like TypeScript in that it doesn't allow you to change the type of a variable after it's been assigned. But, it doesn't need (or allow) you to specify the type, it's always inferred.

Here are some basic Nix expressions:

# Arithmetic 
1 + 1; # => 2

# Strings
"I'm a string!";
''
  Multi-line strings are fun!
  ${1 + 1}
'';

# Lists
[ 1 2 3 4 ];

# Sets
{ a = 1; b = 2 };

# Functions (always single parameters)
a: a * a;
a: b: a + b;
{ arg, set ? withDefaults }: arg + set;

The function expression is worth pausing on for a second, because you see it a lot in Nix code, and it was very confusing to me at first. I'll write a quick Nix function and then write the same function again in JavaScript to help highlight what's happening:

{ stdenv, fetchurl }:

stdenv.mkDerivation rec {
  pname = "hello";
  version = "2.10";

  src = fetchurl {
    url = "mirror://gnu/hello/${pname}-${version}.tar.gz";
    sha256 = "0ssi1wpaf7plaswqqjwigppsg5fyh99vdlb9kzl7c9lng89ndq1i";
  };

  doCheck = true;

  meta = with stdenv.lib; {
    description = "A program that produces a familiar, friendly greeting";
    longDescription = ''
      GNU Hello is a program that prints "Hello, world!" when you run it.
      It is fully customizable.
    '';
    homepage = https://www.gnu.org/software/hello/manual/;
    changelog = "https://git.savannah.gnu.org/cgit/hello.git/plain/NEWS?h=v${version}";
    license = licenses.gpl3Plus;
    maintainers = [ maintainers.eelco ];
    platforms = platforms.all;
  };
}

And in JavaScript:

export default ({ stdenv, fetchurl }) => {
  const pname = 'hello';
  const version = '2.10';
  return stdenv.mkDerivation({
    pname,
    version,

    src: fetchurl({
      url: `mirror://gnu/hello/${pname}-${version}.tar.gz`,
      sha256: '0ssi1wpaf7plaswqqjwigppsg5fyh99vdlb9kzl7c9lng89ndq1i'
    }),

    doCheck: true,

    meta: {
      description: "A program that produces a familiar, friendly greeting",
      longDescription: `
        GNU Hello is a program that prints "Hello, world!" when you run it.
        It is fully customizable.
      `,
      homepage: 'https://www.gnu.org/software/hello/manual/',
      changelog: `https://git.savannah.gnu.org/cgit/hello.git/plain/NEWS?h=v${version}`,
      license: stdenv.lib.licenses.gpl3Plus,
      maintainers: [ maintainers.eelco ],
      platforms: stdenv.lib.platforms.all
    }
  });
}

Not as crazy as it looks, huh? I think the most confusing thing for me was that I was expecting the stuff that is in the { ... } to be code, but it's just a Set which is very similar to a JavaScript Object. Nix applies functions just by putting the argument next to it, like: someFunc someArg, which would be equivalent to someFunc(someArg).

Another cool thing here is being able to reference keys in the same Set when you include the rec (short for recursive) keyword before the Set literal. 🆒

We'll dig a bit deeper into all of this as I continue this series of posts. In the next post, we'll talk a bit more about what a "derivation" is, and how to write one!

Further reading:

Posted on by:

scotttrinh profile

Scott Trinh

@scotttrinh

Interested in types and team/community organization

Discussion

markdown guide