Functors as flake outputs

In order to expose system specialized outputs, aside from those already handled explicitly, I often found myself doing this ( especially in a repl ):

( builtins.getFlake "foo" ).helper.${system} { ... };
# or
( builtins.getFlake "foo" ).helper { inheret system; ... };
# or
( builtins.getFlake "foo" ).helper system { ... };

I found that having both an attribute for each system and a function was really convenient, and started writing flake outputs with __functor members to accept system as an arg.

With this in mind I wrote up a convenient helper lib that I wanted to share :slight_smile:

# Example Usage: 
#   nix-repl> add = curryDefaultSystems ( system:
#                     { x, y }: builtins.trace system ( x + y )
#                   ) 
# 
#   nix-repl> add { x = 1; y = 2; } 
#   { __functor      = <lambda>;
#     aarch64-darwin = 3; trace: aarch64-darwin 
#     aarch64-linux  = 3; trace: aarch64-linux 
#     i686-linux     = 3; trace: i686-linux 
#     x86_64-darwin  = 3; trace: x86_64-darwin 
#     x86_64-linux   = 3; trace: x86_64-linux 
#   } 
# 
#   nix-repl> ( add { x = 1; y = 2; } ) "x86_64-linux" 
#   trace: x86_64-linux 
#   3 
  
 { utils ? builtins.getFlake "github:numtide/flake-utils" }: 
 let 
    inherit (utils.lib) eachSystemMap defaultSystems; 
  
    currySystems = supportedSystems: fn: args: let 
       fas    = builtins.functionArgs fn; 
       callAs = system: fn ( { inherit system; } // args ); 
       callV  = system: fn system args;
       apply  = if ( fas == {} ) then callV else if ( fas ? system ) then callAs 
                else throw "provided function cannot accept system as an arg"; 
       sysAttrs = eachSystemMap supportedSystems apply; 
       curried  = { __functor = self: system: self.${system}; }; 
    in sysAttrs // curried;
  
    curryDefaultSystems = currySystems defaultSystems;

    funkSystems = supportedSystems: fn: let 
       fas    = builtins.functionArgs fn; 
       callAs = system: fn { inherit system; }; 
       callV  = system: fn system; 
       apply  = if ( fas == {} ) then callV else if ( fas ? system ) then callAs 
                else throw "provided function cannot accept system as an arg"; 
       sysAttrs = eachSystemMap supportedSystems apply; 
       curried  = { __functor = self: system: self.${system}; }; 
    in sysAttrs // curried; 
  
    funkDefaultSystems = funkSystems defaultSystems;
 
in {   
    inherit currySystems curryDefaultSystems; 
    inherit funkSystems funkDefaultSystems; 
 }  /* End `attrsets.nix' */

Here are two examples of the pattern “in place” without the polymorphism, these can be rewritten with the helpers:

outputs = {...}: let ... in {
  # Wrappers for Pandoc, Makeinfo, and NixOS module options' generators.
  # Multiple helper functions in an attribute set, which share
  # top-level arguments.
  docgen = ( eachDefaultSystemMap ( system: import ./pkgs/docgen { 
    inherit (nixpkgs.legacyPackages.${system}) pandoc texinfo; 
  } ) ) // { __functor = docgenSelf: system: docgenSelf.${system}; }; 
  
  # A single function, which stashes system specific args
  # and accepts remaining ones.
  tarutil = ( eachDefaultSystemMap ( system:
    { compressor ? nixpkgs.legacyPackages.${system}.gzip }:
    import ./pkgs/build-support/trivial/tar.nix { 
      inherit system compressor;
      inherit (nixpkgs.legacyPackages.${system}) gnutar; 
    } ) ) // { __functor = tarSelf: system: tarSelf.${system}; };
} 

In a REPL or legacy Nix files I can use either style depending on what’s more ergonomic:

nix-repl> :a map ( x: x.x86_64-linux ) {
            inherit ( builtins.getFlake "foo" ) bar baz;
          }
nix-repl> bar {...}
nix-repl> # or
nix-repl> ( builtins.getFlake "foo" ).quux {
            inheret (somePkg) system buildInputs version;
          }

You may be thinking at this point: “if this guy thinks this is a novel idea, wait till he hears about overlays”; which is a fair point. I was previously using overlays for cases like this; but counterpoint - this is hideous and is slower to eval:

nix-repl> ( ( builtins.getFlake "nixpkgs" ).overlays = [
            ( builtins.getFlake "foo" ).overlays.default
          ] ).quux { inheret (somePkg) builtInputs version; }

So yeah, I’m aware that this isn’t “fixing a missing feature”; but it sure is convenient!

3 Likes