Alternative language

Here are a few language properties that are quite useful to have:

Read-only (no side-effects)

One of the key characteristics of nix is that there are no side-effects to evaluating the language. As long as you have the same inputs you will get the same build recipes. The only place that can be written into is the /nix/store.

This is important to be able to reason about the code and keep it reproducible. I don’t know of other languages that have this builtin as they are mostly designed to interact with the outside world and writing to files it most likely part of the stdlib.

Lazily evaluated

Unlike most other languages, Nix code is only really evaluated when the values are accessed. For example you can write { foo = throw "exception"; bar = 4; }. As long as the “foo” key doesn’t get accessed, the exception won’t be raised.

This can be quite annoying for debugging because it’s much harder to construct stack-traces that makes sense compared to imperative languages. Most people are also unfamiliar with that mental model and tend to read code linearly.

On the other hand, I think this is quite crucial for composing different pieces together. Things like overlays, package overrides and nixos modules all depend on the lazy evaluation to work nicely. This is also important to avoid evaluating all of nixpkgs all the time because it’s so large.

With an imperative language, this can be solved with a level of indirection. Basically add a function call that delays the import of the rest of nixpkgs. This works but all indirection has to be added manually. If one is missing then it means changing the interface.

I don’t know of other languages that have a lazy evaluation model except Haskell.

String context

This is a mostly invisible feature of nix, I don’t think many people know about this.

Strings in nix can have some context attached to them and which is propagated to other strings when they get concatenated together. That context is used to automatically keep track of which derivations a string has been issued from, and then injected into other derivations when they get declared.

For example: pkgs.runCommand "say-hello" {} "echo ${pkgs.cowsay}/bin/cowsay Hello > $out". This is a derivation that outputs a ascii-art cow that says “Hello”. The interesting part is that pkgs.cowsay automatically becomes part of the build of the “say-hello” derivation, just because there is a reference to it in the build recipe. Because the string interpolation casts the pkgs.cowsay to a string, which contains a reference to the pkgs.cowsay derivation in it’s context, which gets propagated to the surrounding string which is added to the derivation produced by the runCommand function. Again, this helps compose derivations together easily. Without needing a ceremony like an interface to declare all the build inputs.

Conclusion

Over time I came to appreciate those properties. I think they are what make the language quite unique and I can’t think of any other existing languages on which those could easily be retro-fitted onto. If anyone has one let me know :slight_smile:

24 Likes