Contracts in Nix
NEW: Now contracts are compatible with yants .
The Nix language lacks a good type system. There are already several configuration languages that provide static and even gradual typing (see, e.g., Cue, Dhall, or Nickel), but none offer the ability to easily annotate legacy Nix code with types.
“Contracts”, which you can now define thanks to the utilities offered in this library, come to the rescue! And because an example is worth a thousand words:
{ sources ? import nix/sources.nix } : with import sources . contracts { enable = true ; }; sourcescontracts # Describe fields of package.json we will later need, so if the error comes # from a malformed file, we will fail early: let package = contract { message = _ : "`package.json' malformed..." ; } { contract bundleDependencies = enum [ Bool ( listOf Str ) ]; enumBoollistOf Str dependencies = setOf Str ; setOf Str } ( builtins . fromJSON ( builtins . readFile ./package.json )); fromJSONreadFile # We trust the data so we can write simpler logic, even with weird specifications: # https://docs.npmjs.com/cli/v8/configuring-npm/package-json#bundledependencies deps = with package ; package if Bool bundleDependencies then Bool bundleDependencies if bundleDependencies then dependencies bundleDependenciesdependencies else {} else let filterAttrsName = with builtins ; set : xs : removeAttrs set ( partition ( x : elem x xs ) ( attrNames set )). wrong ; in setpartitionelem x xsattrNames setwrong ; filterAttrsName dependencies bundleDependencies # I leave the writing of `bundler` (or any working derivation) to the reader! # Notice that `nixpkgs` wasn't required until now: pkgs = import sources . nixpkgs {}; in derivation { sourcesnixpkgs name = "this-is-just-a-dumb-example" ; builder = " ${ pkgs . bash } /bin/bash" ; pkgsbash args = [ ./bundler ( builtins . toFile "deps.json" ( builtins . toJSON deps )) ]; toFiletoJSON deps system = builtins . currentSystem ; currentSystem }
What’s behind such dark magic? Basic ideas (and a few lines of code):
The expressiveness of the Nix language is greater than what we expect of most type systems, and good news: Nix expression computation is expected to terminate (it’s not perfect, indeed, but did you know that C++ template system resolution can loop infinitely?).
Language builtins already offer what is needed to compare primitive types and to unpack more complex ones! And those who have played with nixpkgs constructs know that there is something like (runtime types) in lib.types to define mkOption . (I provide insights below on how these two models interoperate and, of course, why this one is greater.)
And the deadly simple concept of “Validators”, a function that takes arbitrary data and returns a boolean if the data is correct. Developers write validators on a weekly basis; it’s what you’re doing, e.g., when you use a regex to check if a string is a valid URL, or when you check if a value is not null . Now think about having a type Url or a type Not Null ?
nix-repl> Url "Hello, " false nix-repl> not Null "World!" true
... continue reading