From d8ac889ba63f8f8e229ff537eb274d226610b4f6 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 9 Feb 2022 13:57:40 +0100 Subject: [PATCH] lib: Add encapsulate Creates objects that have overlay-based private attrs in their closure. (cherry picked from commit 0067cf4fc2ff48a4cf59e961c1917e9bf2faec36) --- lib/default.nix | 3 +- lib/fixed-points.nix | 90 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/lib/default.nix b/lib/default.nix index 668c29640f9f1b6..873fc235bc6b2be 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -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 diff --git a/lib/fixed-points.nix b/lib/fixed-points.nix index 3bd18fdd2a5a9ef..bf2b5bbf9bd8700 100644 --- a/lib/fixed-points.nix +++ b/lib/fixed-points.nix @@ -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; + }