36f6322d16
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
|
|
;
|
|
}
|