I've had the notion that builtins.genericClosure can be used to express any recursive algorithm, but a proof is much better than a notion of course! In this case we can easily show this by implementing a function that converts a tail recursive function into an application of builtins.genericClosure. This is possible if the function resolves its self reference using a fixed point which allows us to pass a function that encodes the call to self in a returned attribute set, leaving the actual call to genericClosure's operator. Additionally, some tools for collecting meta data about functions (argCount) and calling arbitrary functions (apply, unapply) are necessary. Change-Id: I7d455db66d0a55e8639856ccc207639d371a5eb8 Reviewed-on: https://cl.tvl.fyi/c/depot/+/5292 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org> Autosubmit: sterni <sternenseemann@systemli.org>
		
			
				
	
	
		
			257 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			257 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
| { depot, lib, ... }:
 | |
| 
 | |
| let
 | |
| 
 | |
|   inherit (lib)
 | |
|     id
 | |
|     ;
 | |
| 
 | |
|   # Simple function composition,
 | |
|   # application is right to left.
 | |
|   rl = f1: f2:
 | |
|     (x: f1 (f2 x));
 | |
| 
 | |
|   # Compose a list of functions,
 | |
|   # application is right to left.
 | |
|   rls = fs:
 | |
|     builtins.foldl' (fOut: f: lr f fOut) id fs;
 | |
| 
 | |
|   # Simple function composition,
 | |
|   # application is left to right.
 | |
|   lr = f1: f2:
 | |
|     (x: f2 (f1 x));
 | |
| 
 | |
|   # Compose a list of functions,
 | |
|   # application is left to right
 | |
|   lrs = x: fs:
 | |
|     builtins.foldl' (v: f: f v) x fs;
 | |
| 
 | |
|   # Warning: cursed function
 | |
|   #
 | |
|   # Check if a function has an attribute
 | |
|   # set pattern with an ellipsis as its argument.
 | |
|   #
 | |
|   # s/o to puck for discovering that you could use
 | |
|   # builtins.toXML to introspect functions more than
 | |
|   # you should be able to in Nix.
 | |
|   hasEllipsis = f:
 | |
|     builtins.isFunction f &&
 | |
|     builtins.match ".*<attrspat ellipsis=\"1\">.*"
 | |
|       (builtins.toXML f) != null;
 | |
| 
 | |
|   /* Return the number of arguments the given function accepts or 0 if the value
 | |
|      is not a function.
 | |
| 
 | |
|      Example:
 | |
| 
 | |
|        argCount argCount
 | |
|        => 1
 | |
| 
 | |
|        argCount builtins.add
 | |
|        => 2
 | |
| 
 | |
|        argCount pkgs.stdenv.mkDerivation
 | |
|        => 1
 | |
|   */
 | |
|   argCount = f:
 | |
|     let
 | |
|       # N.B. since we are only interested if the result of calling is a function
 | |
|       # as opposed to a normal value or evaluation failure, we never need to
 | |
|       # check success, as value will be false (i.e. not a function) in the
 | |
|       # failure case.
 | |
|       called = builtins.tryEval (
 | |
|         f (builtins.throw "You should never see this error message")
 | |
|       );
 | |
|     in
 | |
|     if !(builtins.isFunction f || builtins.isFunction (f.__functor or null))
 | |
|     then 0
 | |
|     else 1 + argCount called.value;
 | |
| 
 | |
|   /* Call a given function with a given list of arguments.
 | |
| 
 | |
|      Example:
 | |
| 
 | |
|        apply builtins.sub [ 20 10 ]
 | |
|        => 10
 | |
|   */
 | |
|   apply = f: args:
 | |
|     builtins.foldl' (f: x: f x) f args;
 | |
| 
 | |
|   # TODO(sterni): think of a better name for unapply
 | |
|   /* Collect n arguments into a list and pass them to the given function.
 | |
|      Allows calling a function that expects a list by feeding it the list
 | |
|      elements individually as function arguments - the limitation is
 | |
|      that the list must be of constant length.
 | |
| 
 | |
|      This is mainly useful for functions that wrap other, arbitrary functions
 | |
|      in conjunction with argCount and apply, since lists of arguments are
 | |
|      easier to deal with usually.
 | |
| 
 | |
|      Example:
 | |
| 
 | |
|        (unapply 3 lib.id) 1 2 3
 | |
|        => [ 1 2 3 ]
 | |
| 
 | |
|        (unapply 5 lib.reverse) 1 2 null 4 5
 | |
|        => [ 5 4 null 2 1 ]
 | |
| 
 | |
|        # unapply and apply compose the identity relation together
 | |
| 
 | |
|        unapply (argCount f) (apply f)
 | |
|        # is equivalent to f (if the function has a constant number of arguments)
 | |
| 
 | |
|        (unapply 2 (apply builtins.sub)) 20 10
 | |
|        => 10
 | |
|   */
 | |
|   unapply =
 | |
|     let
 | |
|       unapply' = acc: n: f: x:
 | |
|         if n == 1
 | |
|         then f (acc ++ [ x ])
 | |
|         else unapply' (acc ++ [ x ]) (n - 1) f;
 | |
|     in
 | |
|     unapply' [ ];
 | |
| 
 | |
|   /* Optimize a tail recursive Nix function by intercepting the recursive
 | |
|      function application and expressing it in terms of builtins.genericClosure
 | |
|      instead. The main benefit of this optimization is that even a naively
 | |
|      written recursive algorithm won't overflow the stack.
 | |
| 
 | |
|      For this to work the following things prerequisites are necessary:
 | |
| 
 | |
|      - The passed function needs to be a fix point for its self reference,
 | |
|        i. e. the argument to tailCallOpt needs to be of the form
 | |
|        `self: # function body that uses self to call itself`.
 | |
|        This is because tailCallOpt needs to manipulate the call to self
 | |
|        which otherwise wouldn't be possible due to Nix's lexical scoping.
 | |
| 
 | |
|      - The passed function may only call itself as a tail call, all other
 | |
|        forms of recursions will fail evaluation.
 | |
| 
 | |
|      This function was mainly written to prove that builtins.genericClosure
 | |
|      can be used to express any (tail) recursive algorithm. It can be used
 | |
|      to avoid stack overflows for deeply recursive, but naively written
 | |
|      functions (in the context of Nix this mainly means using recursion
 | |
|      instead of (ab)using more performant and less limited builtins).
 | |
|      A better alternative to using this function is probably translating
 | |
|      the algorithm to builtins.genericClosure manually. Also note that
 | |
|      using tailCallOpt doesn't mean that the stack won't ever overflow:
 | |
|      Data structures, especially lazy ones, can still cause all the
 | |
|      available stack space to be consumed.
 | |
| 
 | |
|      The optimization also only concerns avoiding stack overflows,
 | |
|      tailCallOpt will make functions slower if anything.
 | |
| 
 | |
|      Type: (F -> F) -> F where F is any tail recursive function.
 | |
| 
 | |
|      Example:
 | |
| 
 | |
|      let
 | |
|        label' = self: acc: n:
 | |
|          if n == 0
 | |
|          then "This is " + acc + "cursed."
 | |
|          else self (acc + "very ") (n - 1);
 | |
| 
 | |
|        # Equivalent to a naive recursive implementation in Nix
 | |
|        label = (lib.fix label') "";
 | |
| 
 | |
|        labelOpt = (tailCallOpt label') "";
 | |
|      in
 | |
| 
 | |
|      label 5
 | |
|      => "This is very very very very very cursed."
 | |
| 
 | |
|      labelOpt 5
 | |
|      => "This is very very very very very cursed."
 | |
| 
 | |
|      label 10000
 | |
|      => error: stack overflow (possible infinite recursion)
 | |
| 
 | |
|      labelOpt 10000
 | |
|      => "This is very very very very very very very very very…
 | |
|   */
 | |
|   tailCallOpt = f:
 | |
|     let
 | |
|       argc = argCount (lib.fix f);
 | |
| 
 | |
|       # This function simulates being f for f's self reference. Instead of
 | |
|       # recursing, it will just return the arguments received as a specially
 | |
|       # tagged set, so the recursion step can be performed later.
 | |
|       fakef = unapply argc (args: {
 | |
|         __tailCall = true;
 | |
|         inherit args;
 | |
|       });
 | |
|       # Pass fakef to f so that it'll be called instead of recursing, ensuring
 | |
|       # only one recursion step is performed at a time.
 | |
|       encodedf = f fakef;
 | |
| 
 | |
|       opt = args:
 | |
|         let
 | |
|           steps = builtins.genericClosure {
 | |
|             # This is how we encode a (tail) call: A set with final == false
 | |
|             # and the list of arguments to pass to be found in args.
 | |
|             startSet = [
 | |
|               {
 | |
|                 key = "0";
 | |
|                 id = 0;
 | |
|                 final = false;
 | |
|                 inherit args;
 | |
|               }
 | |
|             ];
 | |
| 
 | |
|             operator =
 | |
|               { id, final, ... }@state:
 | |
|               let
 | |
|                 # Plumbing to make genericClosure happy
 | |
|                 newIds = {
 | |
|                   key = toString (id + 1);
 | |
|                   id = id + 1;
 | |
|                 };
 | |
| 
 | |
|                 # Perform recursion step
 | |
|                 call = apply encodedf state.args;
 | |
| 
 | |
|                 # If call encodes a new call, return the new encoded call,
 | |
|                 # otherwise signal that we're done.
 | |
|                 newState =
 | |
|                   if builtins.isAttrs call && call.__tailCall or false
 | |
|                   then newIds // {
 | |
|                     final = false;
 | |
|                     inherit (call) args;
 | |
|                   } else newIds // {
 | |
|                     final = true;
 | |
|                     value = call;
 | |
|                   };
 | |
|               in
 | |
| 
 | |
|               if final
 | |
|               then [ ] # end condition for genericClosure
 | |
|               else [ newState ];
 | |
|           };
 | |
|         in
 | |
|         # The returned list contains intermediate steps we ignore.
 | |
|         (builtins.head (builtins.filter (x: x.final) steps)).value;
 | |
|     in
 | |
|     unapply argc opt;
 | |
| in
 | |
| 
 | |
| {
 | |
|   inherit (lib)
 | |
|     fix
 | |
|     flip
 | |
|     const
 | |
|     ;
 | |
| 
 | |
|   inherit
 | |
|     id
 | |
|     rl
 | |
|     rls
 | |
|     lr
 | |
|     lrs
 | |
|     hasEllipsis
 | |
|     argCount
 | |
|     tailCallOpt
 | |
|     apply
 | |
|     unapply
 | |
|     ;
 | |
| }
 |