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.fix: Improve doc #242318

Merged
merged 2 commits into from
Oct 12, 2023
Merged

lib.fix: Improve doc #242318

merged 2 commits into from
Oct 12, 2023

Conversation

roberth
Copy link
Member

@roberth roberth commented Jul 8, 2023

The original doc did not help with understanding much, and the wikipedia link was actively harmful.

Description of changes

Starts with succinct reference docs for what is TECHNICALLY a simple function, then goes into explanation.

I wish I could stress "technically" even more. Basically all of its complexity is emergent, as it mirrors a math concept that most programmers are not familiar with, in a language paradigm many aren't familiar with either.

However, it is self-contained and that makes this place a natural location for the explanation.

Things done
  • Built on platform(s)
    • x86_64-linux
    • aarch64-linux
    • x86_64-darwin
    • aarch64-darwin
  • For non-Linux: Is sandbox = true set in nix.conf? (See Nix manual)
  • Tested, as applicable:
  • Tested compilation of all packages that depend on this change using nix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD". Note: all changes have to be committed, also see nixpkgs-review usage
  • Tested basic functionality of all binary files (usually in ./result/bin/)
  • 23.11 Release Notes (or backporting 23.05 Release notes)
    • (Package updates) Added a release notes entry if the change is major or breaking
    • (Module updates) Added a release notes entry if the change is significant
    • (Module addition) Added a release notes entry if adding a new NixOS module
  • Fits CONTRIBUTING.md.

The original doc did not help with understanding at all, and the
wikipedia link was actively harmful.
@roberth roberth added the 6.topic: lib The Nixpkgs function library label Jul 8, 2023
@roberth roberth requested review from fricklerhandwerk and removed request for edolstra and infinisil July 8, 2023 18:17
Copy link
Contributor

@tjni tjni left a comment

Choose a reason for hiding this comment

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

I like the fleshed out explanation building up to how this can work. Thank you.

I left some comments but they are just my opinions and need not be followed up on (or followed up on before merging this PR).

Compute the fixed point of the given function `f`, which is usually an
attribute set that expects its final, non-recursive representation as an
argument:
`fix f` computes the fixed point of the given function `f`. In other words, the return value is `x` in `x = f x`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I think it reads a little better like: "In other words, it returns the value x satisfying x = f x."


`f` is usually returns an attribute set that expects its final, non-recursive representation as an argument.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: "is usually" => "usually"


`f` is usually returns an attribute set that expects its final, non-recursive representation as an argument.
`f` must be a lazy function.
Copy link
Contributor

Choose a reason for hiding this comment

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

I personally feel this lazy explanation would be more powerful somewhere in the "How it works" section to give some insight into how it's possible to compute the fixed point.


```
nix-repl> fix f
{ bar = "bar"; foo = "foo"; foobar = "foobar"; }
```

Type: fix :: (a -> a) -> a
This example did not _need_ `fix`, and arguably it shouldn't be used in such an example.
However, `fix` is useful when your `f` is a parameter, or when it is constructed from higher order functions.
Copy link
Contributor

Choose a reason for hiding this comment

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

To me, this is an area that can be expanded. To this point, the only example in my mind is the rec attribute set, so f just seems like another way to represent that. It feels like I could pass such an attribute set as a parameter or perhaps construct one from higher order functions without having to pass through fix.

Copy link
Member Author

Choose a reason for hiding this comment

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

We now refer to extends as a use case.

Copy link
Contributor

@fricklerhandwerk fricklerhandwerk left a comment

Choose a reason for hiding this comment

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

Somewhere in there is hidden a great introduction to the module system, but it's not visible yet.


```
nix-repl> fix f
{ bar = "bar"; foo = "foo"; foobar = "foobar"; }
```

Type: fix :: (a -> a) -> a
This example did not _need_ `fix`, and arguably it shouldn't be used in such an example.
Copy link
Contributor

@fricklerhandwerk fricklerhandwerk Jul 8, 2023

Choose a reason for hiding this comment

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

Why did we choose this example then if it's not helpful to understand practical applications of fix?


```
nix-repl> fix f
{ bar = "bar"; foo = "foo"; foobar = "foobar"; }
```

Type: fix :: (a -> a) -> a
This example did not _need_ `fix`, and arguably it shouldn't be used in such an example.
However, `fix` is useful when your `f` is a parameter, or when it is constructed from higher order functions.
Copy link
Contributor

Choose a reason for hiding this comment

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

I can only guess at what this means. Is it when f is a parameter in a scope where we want to evaluate a recursive attrset and therefore we want to abstract over that? (And still why would we do it that way instead of defining a rec set?) What would f constructed from higher order functions look like? Please make an example for each.

Comment on lines 48 to 51
```nix
fix = f:
let self = f self; in self;
```
Copy link
Contributor

Choose a reason for hiding this comment

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

What helped me understand this is an eval trace where we apply x to f and see how self is replaced by x, yielding the let expression in the previous sample.

```

Nix evaluates this recursion until all references to `self` have been
resolved. At that point, the final result is returned and `f x = x` holds:
`let` bindings are nice, but as it is with `let` bindings in general, we may get more reuse out of the code by defining a function.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a very handwavy motivation. It does not explain at all why we would go down that path.

Compute the fixed point of the given function `f`, which is usually an
attribute set that expects its final, non-recursive representation as an
argument:
`fix f` computes the fixed point of the given function `f`. In other words, the return value is `x` in `x = f x`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
`fix f` computes the fixed point of the given function `f`. In other words, the return value is `x` in `x = f x`.
`fix f` computes the fixed point of the given function `f`.
This is the value that satisfies `f x == x`.

"In other words" is the function definition, we already have that and it doesn't help.

What's missing here though is a non-magical explanation of how that value is found and when it works at all and why.

For instance, you can't say fix (x: 1.0 / x) although clearly the answer should be 1.

You also can't say fix ({a,b,...}: {a=1;b=2;c=a+b;}) for reasons unclear to me.

Comment on lines 4 to 7
`fix f` computes the fixed point of the given function `f`. In other words, the return value is `x` in `x = f x`.

`f` is usually returns an attribute set that expects its final, non-recursive representation as an argument.
`f` must be a lazy function.
Copy link
Member

@infinisil infinisil Jul 10, 2023

Choose a reason for hiding this comment

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

Suggested change
`fix f` computes the fixed point of the given function `f`. In other words, the return value is `x` in `x = f x`.
`f` is usually returns an attribute set that expects its final, non-recursive representation as an argument.
`f` must be a lazy function.
`fix f` computes `x` such that `x = f x`, also known as the fixed point of `f`.
In other words, it calls `f` with the result of itself.
This can only be meaningful when `x` is a value that can be partially evaluated, such as an attribute set, a list, or a function.
This way, `f` can use one part of `x` to compute another part.

I'd suggest a wording something like this.

An example with a list and a function would also be good.

Copy link
Contributor

@fricklerhandwerk fricklerhandwerk Jul 10, 2023

Choose a reason for hiding this comment

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

This is great!

Suggested change
`fix f` computes the fixed point of the given function `f`. In other words, the return value is `x` in `x = f x`.
`f` is usually returns an attribute set that expects its final, non-recursive representation as an argument.
`f` must be a lazy function.
`fix f` computes `x` such that `x == f x`, also known as the fixed point of `f`.
This can only be meaningful when `x` is a value that can be partially evaluated, such as an attribute set, a list, or a function.
This way, `f` can use one part of `x` to compute another part.

I'd remove the "in other words" because it misleads the reader into tying brain knots.

Copy link
Member

Choose a reason for hiding this comment

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

I'd rather you don't edit my own comment fwiw! (why does GitHub even allow that though..) I edited it back now.

In particular, the "This can only be meaningful when" part is intentional, it should not be "This is only meaningful when". Because just using e.g. an attribute set in the result doesn't mean you'll get something "meaningful", e.g. f (attrs: { x = attrs.x + 1; }).

The only thing we know here is that if you don't use an attribute set, a list or a function, it's definitely going to either result in an infinite recursion (f (x: x + 1)), or in a constant value (f (x: 1)), which is definitely not meaningful.

@nixos-discourse
Copy link

This pull request has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/2023-07-13-documentation-team-meeting-notes-63/30450/1

@infinisil
Copy link
Member

See also my related proposed rewrite of the extends documentation in #248220

Done together in and after the docs team meeting

Co-Authored-By: Robert Hensing <robert@roberthensing.nl>
Copy link
Member

@infinisil infinisil left a comment

Choose a reason for hiding this comment

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

Worked on this together in the docs team meeting today, approved!

@nixos-discourse
Copy link

This pull request has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/2023-10-12-documentation-team-meeting-notes-86/34118/1

@infinisil infinisil merged commit b597bff into NixOS:master Oct 12, 2023
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants