Skip to content

Commit

Permalink
lib: Add encapsulate
Browse files Browse the repository at this point in the history
Creates objects that have overlay-based private attrs in their closure.

(cherry picked from commit 0067cf4)
  • Loading branch information
roberth committed Mar 17, 2024
1 parent 8437d13 commit d8ac889
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 1 deletion.
3 changes: 2 additions & 1 deletion lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ let
functionArgs setFunctionArgs isFunction toFunction mirrorFunctionArgs
toHexString toBaseDigits inPureEvalMode;
inherit (self.fixedPoints) fix fix' converge extends composeExtensions
composeManyExtensions makeExtensible makeExtensibleWithCustomName;
composeManyExtensions makeExtensible makeExtensibleWithCustomName
encapsulate;
inherit (self.attrsets) attrByPath hasAttrByPath setAttrByPath
getAttrFromPath attrVals attrValues getAttrs catAttrs filterAttrs
filterAttrsRecursive foldlAttrs foldAttrs collect nameValuePair mapAttrs
Expand Down
90 changes: 90 additions & 0 deletions lib/fixed-points.nix
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,94 @@ rec {
fix' (self: (rattrs self) // {
${extenderName} = f: makeExtensibleWithCustomName extenderName (extends f rattrs);
});

/*
Creates an overridable attrset with encapsulation.
This is like `makeExtensible`, but only the `public` attribute of the fixed
point is returned.
Synopsis:
r = encapsulate (final@{extend, ...}: {
# ... private attributes for `final` ...
public = {
# ... returned attributes for r, in terms of `final` ...
inherit extend; # optional, don't invoke too often; see below
};
})
s = r.extend (final: previous: {
# ... updates to private attributes ...
# optionally
public = previous.public // {
# ... updates to public attributes ...
};
})
= Performance
The `extend` function evaluates the whole fixed point all over, reusing
no "intermediate results" from the existing object.
This is necessary, because `final` has changed.
So the cost is quadratic; O(n^2) where n = number of chained invocations.
This has consequences for interface design.
Although enticing, `extend` is not suitable for directly implementing "fluent interfaces", where the caller makes many calls to `extend` via domain-specific "setters" or `with*` functions.
Fluent interfaces can not be implemented efficiently in Nix and have very little to offer over attribute sets in terms of usability.*
Example:
# cd nixpkgs; nix repl lib
nix-repl> multiplier = encapsulate (self: {
a = 1;
b = 1;
public = {
r = self.a * self.b;
# Publishing extend makes the attrset open for any kind of change.
inherit (self) extend;
# Instead, or additionally, you can add domain-specific functions.
# Offer a single method with multiple arguments, and not a
# "fluent interface" of a method per argument, because all extension
# functions are called for every `extend`. See the Performance section.
withParams = args@{ a ? null, b ? null }: # NB: defaults are not used
self.extend (self: super: args);
};
})
nix-repl> multiplier
{ extend = «lambda»; r = 1; withParams =«lambda»; }
nix-repl> multiplier.withParams { a = 42; b = 10; }
{ extend = «lambda»; r = 420; withParams =«lambda»; }
nix-repl> multiplier3 = multiplier.extend (self: super: {
c = 1;
public = super.public // {
r = super.public.r * self.c;
};
})
nix-repl> multiplier3.extend (self: super: { a = 2; b = 3; c = 10; })
{ extend = «lambda»; r = 60; withParams =«lambda»; }
(*) Final note on Fluent APIs: While the asymptotic complexity can be fixed
by avoiding overlay extension or perhaps using it only at the end of the
chain only, one problem remains. Every method invocation has to produce
a new, immutable state value, which means copying the whole state up to
that point.
*/
encapsulate = layerZero:
let
fixed = layerZero ({ extend = f: encapsulate (extends f layerZero); } // fixed);
in fixed.public;

}

0 comments on commit d8ac889

Please sign in to comment.