← Back to context

Comment by spease

2 years ago

Regarding your last point, I actually do think that if the syntax to nix were different that it would make it much easier to understand. Though, the fact that it is clearly distinct from, say, Python or JavaScript gives it its own distinct feel as to how 'rigid' it is.

To me the big stumbling block in the language is that it at first appears declarative, like you're writing a data structure that describes your configuration. However, when you start modifying anything about packages, you need to call functions to make changes because you're actually writing a function that's transforming input.

So, you're thinking about what you're trying to do in a declarative, descriptive sense and certain parts of what you're writing are structured as such; but then other parts are structured as a transformation.

Eg you write out a list of packages, but then if you want to change one of those packages, you need to start calling functions. As I mentioned in the Python example below, that can wind up requiring calling `override`, `overrideAttrs`, `overridePythonAttrs`, etc.

Consider this:

  {
    packageOverrides = pkgs: {
      python3 = pkgs.python3.override {
        packageOverrides = python-self: python-super: {
          twitch-python = python-super.twitch-python.overrideAttrs (attrs: {
            patches = (attrs.patches or []) ++ [
              ./twitch-allow-no-token.patch
            ];
          });
        };
      };

      twitch-chat-downloader = pkgs.twitch-chat-downloader.overrideAttrs (attrs: {
        patches = (attrs.patches or []) ++ [
          ./twitch-chat-downloader-no-oauth.patch
        ];
      });
    };
  }

Versus this:

  {
    packageOverrides: pkgs {
      python3: {
        twitch-python: pkgs.python3.twitch-python {
          patches: pkgs.python3.twitch-python.patches or ./twitch-allow-no-token.patch
        }
      },
      twitch-chat-downloader: pkgs.twitch-chat-downloader {
        patches: pkgs.twitch-chat-downloader.patches or ./twitch-chat-downloader-no-oauth.patch
      }
  }

The latter is less "functionally perfect", but there is vastly less cognitive overhead required to lay it out because you are basically just describing your goal, rather than having to keep your goal in mind while implementing it functionally (and keeping exactly how nixpkgs is implemented in mind to correctly use those overrides).

This is just off-the-cuff of what I intuitively expected when I first started using Nix, but it's more what I'd expect of a no-holds-barred purpose-built language for declarative package management. All the override stuff seems like needlessly noisy syntax; you know it's going to be everywhere, you might as well build it into the language.

And it can probably be made even simpler with effort.

> To me the big stumbling block in the language is that it at first appears declarative, like you're writing a data structure that describes your configuration. However, when you start modifying anything about packages, you need to call functions to make changes because you're actually writing a function that's transforming input.

That's a very fair point to make. I have noticed the same and it seems like it's borne out of the way Nix (NixOS more precisely) is built, which is in two layers:

- a first layer of packages and whatnot, which is by and large functional programming, and has the gritty implementation you mention which get exposed when you want to alter some specific things

- a second layer of configuration modules, which takes packages and turns them into a declarative interface

From a Ruby analogy I would compare the first to some form of monkey-patching or otherwise forceful injection and the second one to a nice DSL which exposes clear properties to change some bits

For example, on modules there's `.package` which allows one to override the package to use fairly easily:

  services.tailscale.enable = true;
  services.tailscale.package = let
    old = import(fetchTarball("https://github.com/NixOS/nixpkgs/archive/a695c109a2c.tar.gz")) {};
  in
    old.tailscale;

(taken from this issue https://github.com/NixOS/nixpkgs/issues/245769)

Frequently you get additionalPackages or extraConfig or something in this "DSL", which handles the whacky non-descriptive stuff behind the scenes which really is an implementation detail that should not leak through.

So indeed I feel like Nix packages in general should benefit from a more descriptive interface similar to what NixOS modules expose, so that appending patches or adding configure flags would not be such an ordeal.

Basically this (pseudocode) should be generalised and hidden away:

      # iterate over patch map => mypackage, mypatches
        packageOverrides = python-self: python-super: {
          ${mypackage} = python-super.${mypackage}.overrideAttrs (attrs: {
            patches = (attrs.patches or []) ++ ${mypatches};
          });
        };

So you then would just descriptively pass python3.packagePatches = { twitch-python = [./twitch-allow-no-token.patch] } or something and be done with it. Not saying it's easy for Nix folks to achieve that but it should be doable one way or another. I mean, there's already:

      python_packages = python-packages: [
        python-packages.pip
      ];
      python = pkgs.python39.withPackages python_packages;

It's not out of this world to think there chould be a generalisable extension of that for patches (and more) to fill in the various overrides that exist around Nix derivations.

That would certainly make nixpkgs more approachable.