ELI5 how finalAttrs design pattern works

Please, explain like I am 5 years old how that finalAttrs: {} design pattern works in tandem with stdenv.mkDerivation.

And, possibly, how can it be adapted to work with other frameworks (like, say, buildGoModule).


I was playing with nix repl:

> fun = finalAttrs: { a=1; b = finalAttrs.a+1; c = finalAttrs.b+2;  }

> fun { a=0; }

nix-repl> fun ( { a = 0; })    
{ a = 1; b = 1; c = error: attribute 'b' missing

       at «string»:1:45:

            1|  finalAttrs: { a=1; b = finalAttrs.a+1; c = finalAttrs.b+2;  }
             |                                             ^
       Did you mean a?

nix-repl> fun (fun { a = 0; })
{ a = 1; b = 2; c = 3; }

nix-repl> 

However it didn’t help me too much.

2 Likes

It’s more like this:

> fun = finalAttrs: { a=1; b = finalAttrs.a+1; c = finalAttrs.b+2;  }

> fix = f: let x = f x; in x

> fix fun
{ a = 1; b = 2; c = 4; }

The magic is that fix function. It calls a function with its own result as the argument (that’s what let x = f x does). This is possible because of laziness. So fun returns { a = 1;, b = <thunk>; c = <thunk>; }. If you evaluate b, then it’ll evaluate finalAttrs.a + 1, which means reaching into the attrset that was already returned and pulling out a, which is already 1.

That version of fix is efficient, but it’s functionally equivalent to this:

fix = f: f (fix f)

So maybe you can see how this relates to your observation about fun (fun { a = 0; }), in that it takes it to the extreme and evaluates to infinitely re-apply fun to itself in that manner.

5 Likes