Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib.lazyFunction: init #194514

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ let
importJSON importTOML warn warnIf warnIfNot throwIf throwIfNot checkListOfEnum
info showWarnings nixpkgsVersion version isInOldestRelease
mod compare splitByAndCompare
functionArgs setFunctionArgs isFunction toFunction
functionArgs setFunctionArgs isFunction toFunction lazyFunction
toHexString toBaseDigits inPureEvalMode;
inherit (self.fixedPoints) fix fix' converge extends composeExtensions
composeManyExtensions makeExtensible makeExtensibleWithCustomName;
Expand Down
13 changes: 13 additions & 0 deletions lib/tests/misc.nix
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,19 @@ runTests {
expected = { x = false; };
};

testLazyFunction1 = {
expr = fix (lazyFunction ({ a, b, d }: { a = b; b = 1; c = 2; }));
expected = { a = 1; b = 1; c = 2; };
};

testLazyFunctionNeedsDocumentationUpdate = {
expr = fix (lazyFunction (args@{ a, ... }: { a = "a"; b = args?b; }));
# if b is true, change this test case, but also update the documentation
# to say that it is now supported, and which Nix implementations support it
# (if relevant).
expected = { a = "a"; b = false; };
};

# STRINGS

testConcatMapStrings = {
Expand Down
48 changes: 48 additions & 0 deletions lib/trivial.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{ lib }:

let
inherit (lib) mapAttrs functionArgs setFunctionArgs;
in

rec {

## Simple (higher order) functions
Expand Down Expand Up @@ -467,6 +471,50 @@ rec {
then v
else k: v;

/*
Make a function lazy in its argument.

Normally when you call a function `f` like `{ a, b }: { r = foo a b; }`,
the attribute names of the argument must be evaluated before the evaluator
can return a the (partially evaluated) function body (`{ r = ...; }`).

Comment on lines +479 to +480
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
can return a the (partially evaluated) function body (`{ r = ...; }`).
can return a the (partially evaluated) function body (`{ r = ...; }`).
`lazyFunction` can change such a function to not evaluate the attribute
names of the argument before the body is returned.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've rewritten a lot to make better use of the example instead.

`lazyFunction` wraps this kind of function in such a way that this strict
behavior is cancelled out. With the example `f` above, `lazyFunction f` returns
`arg: f { a = arg.a; b = arg.b; }`.

You can see that this wrapper function always satisfies `f`'s parameter
attributes `a` and `b`, but the expressions for them may still fail later.
For example `arg` may not contain an attribute `a`, or not even be an
attribute set.

Limitations:

`lazyFunction` only works for functions that are either defined using the
"set pattern" Nix syntax, or returned from `lib` functions that preserve
function argument metadata

It is not compatible with ellipsis patterns, `{ ... }:`, because only the
explicitly declared parameters are passed through. For example, the
following will not work: `lazyFunction(arg@{ a, ... }: arg.b)`;
even if the caller provides `b`.

It also removes the checks against unexpected arguments.

Example:

lib.fix (lib.lazyFunction ({ a, b }: { a = b; b = 1; }))
*/
lazyFunction = f:
setFunctionArgs
(arg:
f
(mapAttrs
(k: _: arg.${k})
(functionArgs f)
)
)
(functionArgs f) ;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could try to prevent some misuse by throwing an error when functionArgs f is {}, but that might not be worthwhile.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be a problem if someone uses lazyFunction to construct another function where constant functions are a valid argument.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super bad example, but better than no example?

let
  testCase.foo = { a }: a * 2 == a + a;
  testCase.bar = { }: true;
in mapAttrs (k: lazyFunction) testCase


/* Convert the given positive integer to a string of its hexadecimal
representation. For example:

Expand Down