1. 87
  1. 28

    I can think of no shortage of ways in which nix could be improved but it always baffles me when people complain about the nix language, especially since it seems that objections tend to be about its syntax. Perhaps it’s just the Haskeller in me, but to my eyes the syntax seems extremely natural given the language’s domain (targetting pure expressions with of attribute sets, lists, and literals). The syntax is consistent and minimal, focusing its sugar on those areas where it is most needed (namely bindings, where it offers the convenient with and inherit constructs). I can think of few syntactic decisions that I would change if given the opportunity (e.g. perhaps allowing comma-separated lists).

    I suspect the objections to syntax largely stem from poor documentation. While the nix grammar itself is quite small, it is not well explained in any of the introductory documentation. Instead, the nix manual gives page-after-page of prose, documenting each language construct (verbosely) by example. While this has its place, it would be nicely complemented by a single blurb of BNF. This would provide a nice summary of the language’s moving parts and lay bare its simplicity.

    I think the Nix’s biggest problem is not its language but rather nixpkgs and its numerous under-documented conventions and libraries (especially given that these often don’t interact well together; e.g. the override problem). Of course, this is a much harder problem to tackle.

    1. 24

      The problems with the Nix language have nothing to do with its syntax or its documentation (atrocious as that may be). The problems have to do with its semantics.

      The Nix language is lazy in the worst way possible. Evaluating (like say printing) any statement at any time can result in actual operations being carried out. So you can’t print or view anything.

      Nix also uses its laziness in a terrible way. Instead of providing proper APIs, they rely on nesting objects within one another constantly. So you can try to see what an object is, but you quickly discover it contains endless copies of other objects. It’s very hard to build a mental model of what thing you’re dealing with. That’s what makes overrides hard.

      This is all made far worse by the lack of types and definitions for objects. At least in scheme or lisp people use nested objects judiciously and printing doesn’t force promises. In Python I can see what a class contains. In Haskell, I have types, so I know what object I have. Nix is the worst of all worlds. Computations can run at any moment for any reason and you can never know what something is and everything is an unnamed map with random fields attached to it (I’m sorry, a “set”, in Nix they call maps sets).

      But the worst part of Nix the language is how Nix the system uses the concept of laziness. The idea that is that you’re constantly building up a derivation that will be forced by the system at the end. It’s like in C you have a phase where you compile to object files and then link them. But now imagine that the compiling to object files happens randomly, any time you happen to execute any statement, and you can never know if the statement you have compiles something to an object file or is say a number.

      All of this is made worse by how Nix is being used: there are no standard APIs for anything beyond the most basic derivation. Every single language has one or more extremely incompatible and “quirky” ecosystems.

      1. 14

        I used to be a Haskeller. The problem is that Nix misses a lot of the things that makes Haskell good. All the typing is dynamic, and there’s no structural data typing at all. Want to know what kind of value that set you have in the REPL is? Too bad! Look at the property names and kinda guess at it.

        There are no ADTs or even C-style enums, so you have stringly-typed enums everywhere (although the way the module system works mitigates this somewhat).

        The fact that packages are defined as, effectively, functions also makes development frustrating. In order to print out my package in the REPL, I have to call it with the entire environment it needs. And when you’re writing a module with configuration options, that becomes… effectively impossible. Although that’s not really a problem with the language per se.

        1. 10

          As a language, nix doesn’t feel markedly better or worse than various others. However, I think it’s still acceptable to say that I “dislike the language” because I find it difficult to use its de-facto standard library, nixpkgs¹. In my opinion, the standard library can be considered part of the language in most situations. So (as with most programming languages) nix cannot get by purely on its own merit, but is bolstered or dragged down by the libraries it comes with. Unfortunately more so dragged down, in my experience.

          Edit: What I mean to say is, I don’t think I see as many people complaining about the syntax when they complain about “the nix language”. But it’s possible I have a skewed perception of things.

          ¹ And flakes, once considered completed or stable, would be yet another (small) layer of confusion for this “standard library”.

          1. 10

            Space-delimited lists were probably chosen for convenience in simple cases, but they trip me up so often on anything non-trivial:

            # Artificial example
            # This constructs a list containing a derivation, then a function, then a set, then a derivation
            packages = [
              pkgs.foo
              pkgs.bar.override {
                blah blah whatever
              }
              pkgs.baz
            ];
            
            1. 4

              Nix is very nice. I think the problems are mostly:

              • Documentation is too limited, and many popular sources are outdated.

              • The CLI interface is a bit confusing as it has both classic nix commands, nix 2.0 and flakes in the same binary.

              • There’s too much shell code in NixPkgs, which makes it hard to understand.

              • There’s not enough tooling to comprehend the effects of an invocation without running it.

              I am very positive about Nix’s future. I think despite its shortcomings it is the best distro you can run for many workflows. It just needs a bit more of manpower and funding to clean old cruft.

              This is in general a problem with many pieces of software, which evolve too quickly and not enough attention is put into documentation. In an ideal world, Nix would have several full time engineers writing docs.

              1. 3

                Oh and a really frequent misunderstanding is that Nix is implemented in Haskell, and therefore obscure and complicated. Nix certainly borrows many ideas from pure functional languages, and is an excellent package manager for Haskell, but has nothing to do with the language per se.

                1. 2
            2. 13

              For those looking for more detailed examples of just how much difficulty can be encountered by trying to learn and use Nix, I can highly recommend https://ianthehenry.com/posts/how-to-learn-nix/ . Its long, detailed, and can show some of the good and bad outcomes of the deeper issues touched on in the original post.

              1. 11

                I don’t have a huge problem with the Nix configuration language, because I’m familiar with Haskell and other functional programming languages and the Nix configuration language is solidly within that tradition. The documentation , particularly for standard library functions, is very spotty. I still don’t entirely understand what a derivation is, and I’ve had trouble figuring out exactly how to write configuration code that patches a package in a specific way in one context, just from not knowing how to interpret the interpreter error messages.

                1. 10

                  I’m learning Nix lately, and I don’t (yet?) hate the configuration syntax. But I do find the user experience to be on the unfamiliar side, particularly when it comes to terminology. For instance, derivations are Nix’s packages, and everyone calls them packages, but the software doesn’t, so new users can get hung up on that. Better to cut down the learning curve by calling them packages, then teach that Nix packages are different in such and such way.

                  Regarding the configuration language, I wonder if there have been any attempts to translate code in another, more palatable language to Nix? I’m not suggesting a new language like TypeScript. I mean a library in a common language for emitting Nix configuration, the way imperative CDK code yields the CloudFormation YAML that’s so clumsy to write by hand.

                  1. 9

                    If you might like parens more than an MLish DSL (or if you want a full language and not a DSL) come check out Guix :)

                      1. 6

                        If I had a Nickel for every new config language I came across…

                      2. 6

                        For instance, derivations are Nix’s packages, and everyone calls them packages, but the software doesn’t, so new users can get hung up on that.

                        There’s a subtle difference, in that what people usually call “packages” (see e.g. nixpkgs) are higher level expression which get translated to derivations (by nix-instantiate). So the word package is actually quite ambiguous.

                        1. 5

                          I think generating Nix is a bad idea, because Nix already generates shell code, and invokes many subprocesses. So then you’re going to have a leaky abstraction on top of a leaky abstraction.

                          It would be better just to build something directly on top of shell, which is fundamental since all packages themselves use it .

                          And all distros use it, even Nix does. Nix somewhat covers it up, but this long debate shows how central shell is to Nix:

                          https://github.com/NixOS/rfcs/pull/99#

                          From my understanding every package build basically depends on this, and then in the Nix package defs you’ll also see templated shell to fix up specific issues, just like any other package manager:

                          https://github.com/NixOS/nixpkgs/blob/master/pkgs/stdenv/generic/setup.sh

                          1. 6

                            So then you’re going to have a leaky abstraction on top of a leaky abstraction.

                            I still have scars from this. Nix has some cool functionality that basically reimplements Rust’s Cargo in Nix + shell scripts (buildRustCrate). Since Cargo actually supports features with spaces in them (which I hope no crate will ever use), I once worked on a patch that added support for features with spaces to buildRustCrate. IIIRC I eventually got it working, but it was a terrible quotation hell.

                            1. 11

                              Yup exactly, this is the “rewriting upstream” problem, which I mentioned with Bazel here:

                              http://www.oilshell.org/blog/2021/04/build-ci-comments.html#two-problems-with-bazel-and-gg-again

                              Both Bazel and Nix have strong models of the world and assume it’s more homogeneous than it actually is. On the other hand, shell embraces heterogeneity and you can always find a solution without rewriting, and containers add to that flexibility.

                              They should enable correct parallelism, incremental builds, and distribution. (It looks like https://earthly.dev/ is pointing in this direction, though I’ve only read about it and not used it.)

                              Just like Nix prefers rewriting in its expression language; Bazel prefers rewriting with Starklark. You generally throw out all the autoconf and write Starlark instead, which is a lot of work.

                              Many years I reviewed the R build rules for Bazel which my coworker wrote, which is analogous to the Rust problem in Nix (thanks for the example).

                              There is some argument that rewriting is “good”, but simply from spinning our wheels on that work, I no longer have any appetite for it. I’ll concede that upstream doesn’t have all the nice properties of Nix or Bazel, but I also want to spend less time messing with build systems, and I don’t want to clear this hurdle whenever trying out ANY new language (Zig, etc.) I think language-specific package managers are something of a anti-pattern, but that’s the world we live in.

                              We need a “meta-build” system that solves the reproducibility/parallelism/distribution/incrementality problem for everything at once without requiring O(N) rewrites of upstream build systems. I don’t really have any doubt that this will be based on containers.


                              Nix and Bazel have something else in common which I mentioned here – they have a static notion of dependencies that has to be evaluated up front, which can be slow.

                              https://lobste.rs/s/ypwgwp/tvix_we_are_rewriting_nix#c_9wnxyf

                          2. 3

                            The solution some friends and I were kicking around was similar to this. At this point, we’ve got a relatively strong sense of how we like to build our servers and appliances, and we figured it’d be nice just to focus in on the parts we care about (mainly, the ease of specifying packages and users and keys and whatnot) in a JSON blob and then have a script barf out the relevant Nix files and flakes.

                            Sure, we give up like 90% of the bizarre shit Nix gives you, but I honestly don’t need most of that.

                          3. 12

                            Nix is a transitional package-capability system. It aspires to treat package references as capabilities. NixOS isn’t Genode; it cannot prevent forged capabilities. Paths into the Nix store are unguessable, not unforgeable.

                            The first is relatively simple: they developed their own programming language to do configuration, which is not very good and is extremely difficult to learn.

                            I wonder which systems the author is comparing to Nix. Puppet, Ansible, and Terraform have their own configuration languages for describing machines. Debian, Fedora, and Gentoo have their own rule languages for building packages, grown from Make and shell. The author mentions GCL, which is not even lexically scoped and has broken inheritance. The Nix expression language is, in comparison, straightforward and well-documented; it is a lazily-evaluated lambda calculus with datatypes oriented towards describing and building system packages.

                            A better complaint would have been that the reference implementation is slow.

                            The second flaw is that NixOS does not actually provide real isolation.

                            More to the point, GNU/Linux does not actually provide real isolation. Two processes running under distinct users may still exhibit plan interference.

                            1. 14

                              What good does it do to belittle somebody’s difficult learning experience with a tool?

                              1. 8

                                I’m not minimizing the author’s experiences. I had a difficult learning experience with Nix, too. I also had difficult learning experiences with everything I listed for possible comparison. When the author says “not very good” and “extremely difficult to learn”, I want to know what they are comparing to Nix, and how they are comparing it.

                              2. 3

                                The extreme approach to lazy evaluation plays out as a form of come from in practice which can make understanding and debugging the language at times quite difficult. I think if Nix had dropped the lazy evaluation it would probably have improved the ergonomics of the language at the cost of some of it’s ability to masquerade as a declarative configuration format.

                              3. 6

                                Short post that answered a bunch of longtime questions I had about NixOS, really glad to come across it today

                                1. 5

                                  Last fall, I prototyped a Linux distribution trying to combine a nix-store style package repository with overlayfs Unfortunately, overlayfs becomes very unhappy when you try to overlay too many different paths (with three distinct failure modes, interestingly), which severely limits this approach. I still think that there’s a lot of potential here — overlayfs could be fast for arbitrary numbers of paths if that was a design goal — but it’s not there yet.

                                  I’m interested in the notes for this, mentioned in the footnote. I would have thought OverlayFS could handle many paths because Docker images can have many layers, and it’s the default driver? Or maybe these are separate issues?

                                  FWIW I think we need a “middleground between Nix and Docker”, and I’m approaching this from the opposite side… I just ported my continuous build to Docker and shell scripts (mostly keeping logic in shell), which was useful. It now runs on Github Actions and sourcehut identically, using both docker and podman.

                                  https://github.com/oilshell/oil/tree/master/soil

                                  I experienced all the problems with Docker (slow builds, reproducibility, incorrect caching in both directions!) but also the benefits – it’s a fairly simple upgrade from shell. And you don’t have to rewrite entire build configs, e.g. for Python and R packages.

                                  Related comment on the Tvix rewrite, about a middleground between Nix and Docker (and Bazel and gg): https://lobste.rs/s/ypwgwp/tvix_we_are_rewriting_nix#c_virpl1

                                  1. 3

                                    While in a reasonable docker image you’re going to have maybe 20 layers, if every system package adds a layer, you’ll quickly reach 500 or more layers. For example, the system I’m writing this on has 1870 packages installed, and OverlayFS just isn’t made for these kinds of numbers.

                                    1. 1

                                      The kernel limit is 128 lower layers.

                                      1. 1

                                        Ah OK! That makes sense. Actually the idea I was thinking of was just a compromise that would avoid this problem.

                                        I’m thinking of an executable container format very similar to OCI, maybe called “Dumpling”. And the dumpling would be composed of two components:

                                        1. “horizontal” Layers that use OverlayFS. These come from system package managers that “spray” the files all over /bin/, /lib, etc.
                                        2. “vertical” Slices that use bind mount. These come from packages you compile yourself, or maybe some language package managers that confine things to one dir (?). I would probably put these in something like /dumpling/$package-$version.

                                        So the horizontal layers would contain the C++ compiler and system stuff, which change more slowly. You can’t compose them really, but that’s OK for my purposes.

                                        And then the “slices” you can compose on top, and these are the things that change quickly.

                                        This will work for my use cases, not sure if it will generalize … But I want some increased sharing / composability / differential compression, but I don’t want to buy into a huge ecosystem that I have to write package definitions for. One concrete problem that motivated this is to compile specific versions of every shell for https://www.oilshell.org/ to test against. The tests are detailed and tickle version-specific behavior.

                                    2. 4

                                      GUIX is, as far as I understand, Nix but with scheme as the underlying language. I haven’t played with it yet but my attempts at using Nix ended when I got annoyed with the configuration language. Maybe GUIX is would make me happier.

                                      1. 2

                                        Quick question: where did you get the idea that guix is stylized in all caps. It’s not done on the website, wikipedia entry, or anything else I’ve seen regarding guix so what made you decide that’s how it should be written?

                                        Besides that I think there’s more to it than to say that guix is nix with guile. At the very core Guix inherits the idea of the store and transactional package management from nix but in practice their APIs have diveraged quite a bit. Where a nix package may embed a shell script a guix package would more likely use pure scheme and its gexp based dsl. The guix CLI is also very different from the nix cli so if you go in thinking nix but not you’ll probably leave frustrated.

                                        1. 2

                                          UNIX™ is trademarked capitalised and Guix is inspired by it and looks like it is inspired by that name. GNU is also commonly capitalised.

                                      2. 4

                                        I don’t understand what’s wrong with the Nix language. Sure, the space-delimited lists can get a little annoying, but other than that it’s minimalistic enough to use a configuration language but complex enough to do more complicated things like overriding derivations. Maybe it’s because I learnt Haskell before Nix so the syntax is very familiar to me, whereas I see how it could be a learning curve for other users.

                                        What I find the most annoying is the lack of documentation of some of the nixpkgs functions. Some of the functions provided by import <nixpkgs> {}.lib appear to be the same as the ones builtin to the Nix language and there doesn’t seem to be any clear guidance on when to use which version. I’ve also had to look at the source code to find out the difference between writeTextFile, writeText, writeTextDir, writeScript, and writeScriptBin. The docs explain writeTextFile, but the only documentation for the rest is

                                        Many more commands wrap writeTextFile including writeText, writeTextDir, writeScript, and writeScriptBin. These are convenience functions over writeTextFile.

                                        Additionally, it’s a bit frustrating for me how all the documentation for nixpkgs — the lib functions, how to make a derivation, specific details for building packages in certain languages, how to contribute to nixpkgs, overriding packages/overlays etc — are in one gigantic web page that’s quite slow to load and even slower to search for things in.

                                        1. 3

                                          I’ve used NixOS for several years as a normal user and this seems broadly correct.

                                          Though I’m back to Gentoo now. At least I actually understand that one.

                                          1. 3

                                            The fundamental thing that NixOS gets right is that software is never installed globally. All packages are stored in a content-addressable store — for instance, my editor is stored in the directory “/nix/store/frlxim9yz5qx34ap3iaf55caawgdqkip-neovim-0.5.1/” — the binary, global default configuration, libraries, and everything else included in the vim package exists in that directory.

                                            It’s kind of amazing how this sentence alone helped me understand what NixOS is all about, when I had already casually looked at the NixOS homepage and a couple of blogposts and never got close to this.