From 05b5ee9a7eeed648b10e0cf6d5583a73d3b5c799 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Thu, 21 Jan 2021 23:55:23 +0100 Subject: [PATCH] Diff-Framework: investigate simplification for the most common case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After this long break during the "Covid Year 2020", I pick this clean-up task as a means to fresh up my knowledge about the code base The point to note is, when looking at all the existing diff bindings, seemingly there is a lot of redundancy on some technical details, which do not cary much meaining or relevance at the usage site: - the most prominent case is binding to a collection of DiffMutables hold by smart-ptr - all these objects expose an object identity (getID() function), which can be used as »Matcher« - and all these objects can just delegate to the child's buildMutator() function for entering a recursive mutation. --- src/lib/diff/tree-diff-application.hpp | 2 +- .../diff/tree-mutator-collection-binding.hpp | 35 +++- .../diff/tree-mutator-diffmutable-binding.hpp | 189 ++++++++++++++++++ .../diff/tree-mutator-gen-node-binding.hpp | 2 +- src/lib/diff/tree-mutator.hpp | 22 +- .../diff-tree-application-simple-test.cpp | 2 +- wiki/thinkPad.ichthyo.mm | 186 +++++++++++++++-- 7 files changed, 412 insertions(+), 26 deletions(-) create mode 100644 src/lib/diff/tree-mutator-diffmutable-binding.hpp diff --git a/src/lib/diff/tree-diff-application.hpp b/src/lib/diff/tree-diff-application.hpp index 976d6e8fa..fe9e05ac3 100644 --- a/src/lib/diff/tree-diff-application.hpp +++ b/src/lib/diff/tree-diff-application.hpp @@ -68,7 +68,7 @@ ** ** #### State and nested scopes ** For performance reasons, the diff is applied _in place_, directly mutating the - ** target data structure. This makes the diff application _stateful_ -- and in case of + ** target data structure. This makes the diff application _statefull_ -- and in case of ** a *diff conflict*, the target *will be corrupted*. ** ** Our tree like data structures are conceived as a system of nested scopes. Within diff --git a/src/lib/diff/tree-mutator-collection-binding.hpp b/src/lib/diff/tree-mutator-collection-binding.hpp index 4903c1387..9520bbce4 100644 --- a/src/lib/diff/tree-mutator-collection-binding.hpp +++ b/src/lib/diff/tree-mutator-collection-binding.hpp @@ -31,13 +31,44 @@ ** Each concrete TreeMutator instance will be configured differently, and this ** adaptation is done by implementing binding templates, in the way of building ** blocks, attached and customised through lambdas. It is possible to layer - ** several bindings on top of a single TreeMutator -- and especially this header - ** defines a building block for one such layer, especially for binding to a + ** several bindings on top of a single TreeMutator -- and indeed this header + ** defines a building block for one such layer, specifically for binding to a ** representation of "child objects" managed within a typical STL container. ** ** As a _special case_, binding to a STL map is supported, while this usage is rather ** discouraged, since it contradicts the diff semantics due to intrinsic ordering. ** + ** ## Internal structure + ** + ** The task to set up a binding to a _generic STL collection_ has to face some + ** technical intricacies, leading to a rather involved implementation, which can + ** be hard to understand and maintain. We attempt to address this challenge through + ** a decomposition into several sub-tasks, organised into four levels of abstraction + ** - at the bottom we use an adaptation layer in the form of a traits template, + ** with two concrete specialisations of the ContainerTraits for vector-like + ** and map-like collections + ** - on top of this the CollectionBinding is established to establish a kind of + ** generic access protocol for consuming a collection guided by diff instructions + ** - the third level then holds the actual TreeMutator implementation, embodied into + ** the ChildCollectionMutator template, which in fact translates and delegates + ** any actual access to the underlying collection to its embedded CollectionBinding + ** instance... + ** - which in turn is assembled on the top level, the DSL level, from building blocks + ** provided by the client of this collection binding. The entrance point to this + ** DSL layer is the _DefaultBinding, which is established by wrapping the actual + ** collection into the concrete CollectionBinding at the point where the builder + ** is created. The further DSL verbs on the CollectionBindingBuilder just server + ** to provide or overlay some lambdas to fill in the flexible parts of the binding. + ** + ** And these flexible parts are mostly concerned with the _actual contents_ of the + ** STL collection to be bound. Because, at this point, we can not assume much without + ** loosing genericity. Thus, the user of this binding has to fill in the missing link + ** - to decide if a given diff specification is addressed at this collection binding (»Selector«) + ** - when to consider a concrete content element as a _match_ for the diff specification (»Matcher«) + ** - the way actually to construct a new content element in accordance to the given diff spec (»Constructor«) + ** - the actual implementation of value assignment (optional) + ** - and the recursive entrance into mutation of a specific element within that collection (optional) + ** ** @note the header tree-mutator-collection-binding.hpp was split off for sake of readability ** and is included automatically from bottom of tree-mutator.hpp ** diff --git a/src/lib/diff/tree-mutator-diffmutable-binding.hpp b/src/lib/diff/tree-mutator-diffmutable-binding.hpp new file mode 100644 index 000000000..d790ee253 --- /dev/null +++ b/src/lib/diff/tree-mutator-diffmutable-binding.hpp @@ -0,0 +1,189 @@ +/* + TREE-MUTATOR-DIFFMUTABLE-BINDING.hpp - default configuration to attach a collection of DiffMutable objects + + Copyright (C) Lumiera.org + 2021, Hermann Vosseler + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + + +/** @file tree-mutator-Diffmutable-binding.hpp + ** Special supplement for TreeMutator and the STL collection binding, + ** to provide a shortcut and default wiring for a collection holding + ** [DiffMutable](\ref diff-mutable.hpp) objects -- either directly or + ** by smart-ptr. TreeMutator is a customisable intermediary, which + ** enables otherwise opaque implementation data structures to receive + ** and respond to generic structural change messages ("tree diff"). + ** + ** Each concrete TreeMutator instance will be configured differently, and this + ** adaptation is done by combining various building blocks. One of the most relevant + ** binding cases is to attach to a collection of child objects, which are themselves + ** _recursively diff mutable_. This header is based on the + ** [generic STL collection binding](\ref tree-mutator-collection-binding.hpp) + ** and provides the most common default implementation for a »Matcher« and + ** for building a recursive TreeMutator for the child elements by means of + ** delegating to their DiffMutable::buildMutator() function. An additional + ** requirement for this standard setup to be used is that the objects in the + ** collection must expose a `getID()` function to determine the object identity. + ** + ** @note the header tree-mutator-collection-diffmutable-binding.hpp was split off + ** or sake of readability and is included automatically from bottom of + ** tree-mutator.hpp -- no need to include it explicitly + ** + ** @see tree-mutator-test.cpp + ** @see _DefaultBinding + ** @see TreeMutator::build() + ** + */ + + +#ifndef LIB_DIFF_TREE_MUTATOR_DIFFMUTABLE_BINDING_H +#define LIB_DIFF_TREE_MUTATOR_DIFFMUTABLE_BINDING_H + + +#include "lib/error.hpp" +#include "lib/meta/trait.hpp" +#include "lib/diff/gen-node.hpp" +#include "lib/diff/tree-mutator.hpp" +#include "lib/diff/tree-mutator-collection-binding.hpp" + +namespace lib { +namespace diff{ + + class DiffMutable; + + namespace { // Mutator-Builder decorator components... + + using lib::meta::enable_if; + using lib::meta::Yes_t; + using lib::meta::No_t; + + META_DETECT_FUNCTION(typename X::iterator, begin,(void)); + + template + class can_recursively_bind_DiffMutable + { +// typedef typename Strip::Type Type; + + + + public: + enum { value = false +// is_iterable::value +// or is_const_iterable::value +// or is_noexcept_iterable::value +// or is_const_noexcept_iterable::value + }; + }; + + + template + class Is_DiffMutable + { }; + + template + class Is_wrapped_DiffMutable + { }; + + template + class Can_access_ID + { }; + + + template + struct _AccessID + { + GenNode::ID const& + getID (TAR const& target) + { + throw error::Logic ("Unable to access the target element's object ID. " + "Please define a »Matcher« explicitly by invoking the builder function \"matchElement\"."); + } + }; + template + struct _AccessID>> + { + GenNode::ID const& + getID (TAR const& target) + { + return target.getID(); + } + }; + + + template + struct _AccessTarget + { + DiffMutable& + access (ELM const& elm) + { + throw error::Logic ("Unable to determine if the target is DiffMutable, and how to access it. " + "Please define a »Mutator« explicitly by invoking the builder function \"buildChildMutator\"."); + } + }; + template + struct _AccessTarget>> + { + DiffMutable& + access (ELM const& elm) + { + return elm; + } + }; + template + struct _AccessTarget>> + { + DiffMutable& + access (ELM const& elm) + { + return *elm; + } + }; + + + + /** */ + template + struct _DefaultBinding>> + : private _AccessTarget + , private _AccessID + { + template + static auto + attachTo (COLL& coll) + { + return _EmptyBinding::attachTo(coll) + .matchElement([](GenNode const& spec, ELM const& elm) + { + return spec.idi == getID (access (elm)); + }) + .buildChildMutator ([&](ELM& target, GenNode::ID const&, TreeMutator::Handle buff) -> bool + { + access(target).buildMutator (buff); + return true; + }); + } + }; + + + + + + }//(END)Mutator-Builder decorator components... + +}} // namespace lib::diff +#endif /*LIB_DIFF_TREE_MUTATOR_DIFFMUTABLE_BINDING_H*/ diff --git a/src/lib/diff/tree-mutator-gen-node-binding.hpp b/src/lib/diff/tree-mutator-gen-node-binding.hpp index 275b73bbf..3292ab86d 100644 --- a/src/lib/diff/tree-mutator-gen-node-binding.hpp +++ b/src/lib/diff/tree-mutator-gen-node-binding.hpp @@ -61,8 +61,8 @@ #include "lib/diff/gen-node.hpp" -#include "lib/diff/tree-mutator-collection-binding.hpp" #include "lib/diff/tree-mutator.hpp" +#include "lib/diff/tree-mutator-collection-binding.hpp" #include diff --git a/src/lib/diff/tree-mutator.hpp b/src/lib/diff/tree-mutator.hpp index 6c9817e4b..b6b994797 100644 --- a/src/lib/diff/tree-mutator.hpp +++ b/src/lib/diff/tree-mutator.hpp @@ -136,8 +136,8 @@ namespace diff{ * on arbitrary, hierarchical object-like data. * The TreeMutator exposes two distinct interfaces * - the \em operation API -- similar to what a container exposes -- - * is the entirety of abstract operations that can be done to the - * subsumed, tree like target structure + * is the entirety of abstract operations that can be performed + * on the subsumed, tree like target structure * - the \em binding API allows to link some or all of these generic * activities to concrete manipulations known within target scope. */ @@ -197,8 +197,8 @@ namespace diff{ * to prevent a SEGFAULT. Thus `skipSrc` can not match * and thus can not return anything. Consequently the * `del` implementation has to use `matchSrc` explicitly, - * and the latter must invoke the selector prior to - * performing the local match. */ + * and the latter must invoke the "layer selector" prior + * to performing the local match. */ virtual void skipSrc (GenNode const&) { @@ -412,9 +412,12 @@ namespace diff{ * allows for recursive descent into nested child scopes. On invocation, it has * to build a suitable custom TreeMutator implementation into the provided buffer * (handle), and this nested TreeMutator should be wired with the internal - * representation of the nested scope to enter. The code invoking this closure - * typically pushes the buffer on some internal stack and switches then to use - * this nested mutator until encountering the corresponding `EMU` bracket verb. + * representation of the nested scope to enter. At this point, the implementation + * can safely assume that the given `target` data element has already be checked + * with the configured _matcher closure_, and thus can be considered equivalent + * to the given ID. The code invoking this closure then typically pushes the + * buffer onto some internal stack and switches then to use this nested mutator + * until encountering the corresponding `EMU` bracket verb. * @note the `after(Ref::ATTRIBS)` verb can only processed if the selector responds * correct to a Ref::ATTRIBS spec. The implicit default selector does so, i.e. * it rejects `Ref::ATTRIBS`. Please be sure to accept this token _only_ if @@ -484,7 +487,6 @@ namespace diff{ }} // namespace lib::diff -#endif /*LIB_DIFF_TREE_MUTATOR_H*/ /* == implementation detail headers == */ @@ -492,6 +494,10 @@ namespace diff{ #include "lib/diff/tree-mutator-gen-node-binding.hpp" #include "lib/diff/tree-mutator-attribute-binding.hpp" #include "lib/diff/tree-mutator-collection-binding.hpp" +#include "lib/diff/tree-mutator-diffmutable-binding.hpp" #include "lib/diff/tree-mutator-listener-binding.hpp" #include "lib/diff/tree-mutator-noop-binding.hpp" + + +#endif /*LIB_DIFF_TREE_MUTATOR_H*/ diff --git a/tests/library/diff/diff-tree-application-simple-test.cpp b/tests/library/diff/diff-tree-application-simple-test.cpp index 91bde518a..4c11f3e84 100644 --- a/tests/library/diff/diff-tree-application-simple-test.cpp +++ b/tests/library/diff/diff-tree-application-simple-test.cpp @@ -130,7 +130,7 @@ namespace test{ } - /** @test mutate a Record by applying a the sample diff */ + /** @test mutate a Record by applying the [sample diff](\ref #someDiff) */ void demo_one() { diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index cd106bbe1..f14cab1d1 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -37878,6 +37878,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -42385,7 +42418,7 @@ - + @@ -42486,14 +42519,54 @@ - + - + + + + + + + + +

+ ...zumindest im GUI, wo parktisch alle Empfänger auch ein Tangible (Widget oder Controller) sind +

+ +
+ +
+ + + + + + +

+ ...und genau diese virtuelle Builder-Methode ist üblicherweise der Ort, wo mit einer DSL und über Lambdas das Mapping auf die internen Strukturen des DiffMutable hergestellt wird; deshalb kann auch DiffMutable selber ein sehr schlankes Interface sein; der resultierende TreeMutator ist dann eine für den jeweiligen konkreten Typ aus Bausteinen generierte Hilfsklasse +

+ +
+
+ + + + + + +

+ ...und dort per smart-Ptr.
Für diesen Fall kann die Verdrahtung weitgehend automatisch konfiguiert werden, und man muß eigentlich nur noch den Konstruktor-Aufruf explizit (per Lambda) in das TreeMuator-Binding integrieren. Wenn es mehrere »onion layer« gibt, muß allerdings auch noch ein »Selector« definiert werden, um zu steuern, wo genau dieses Binding angewendet wird, und wo sonst auf einen anderen Layer delegiert wird, z.B. für explizit gebundene Objekt-Attribute. +

+ +
+ +
+
- + - + @@ -42530,7 +42603,7 @@ - + @@ -42574,7 +42647,7 @@

- +
@@ -42719,6 +42792,7 @@ + @@ -42964,7 +43038,7 @@ - + @@ -42986,18 +43060,104 @@ - + - - + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ Das ist verständlich aus der Historie: Zunächst einmal war der TreeMutator gedacht als ein Interface, das der client des Diff-Frameworks zu implementieren hat. Nachdem ich diese Übung aber drei mal gemacht hatte, war mir klar, daß dies zu viel verlangt ist. Denn der TreeMutator ist notwendigerweise stark an den Implementierungs-Ansatz im Diff-Framework gebunden. Das heißt, man kann dieses Interface nur implementieren, wenn man diese interne Funktionsweise verstanden hat. Und das Diff-Framework würde seinen Zweck verfehlen, wenn der Nutzer dieses Wissen haben müßte. Also habe ich über den TreeMutator ein Baukastensystem errichtet, welches über Lambdas in den Anwendungskontext gebunden wird. +

+

+ +

+

+ In einem zweiten Anlauf habe ich schließlich die schon bestehenden, explizitien Implementierungen des TreeMutator-Interfaces allesamt "eingefangen" und durch ein Meshup aus dem Bauskastensystem ersetzt. Daher gibt es jetzt nur noch eine einzige valide Implementierung, nämlich die im Baukasen. Und das soll auch so bleiben. +

+

+ +

+

+ Daher ist es zulässig, sich diese Implementierung anzuschauen: in der Tat geht nämlich dort jedem Aufruf des Mutators ein Suchvorgang voraus, und dieser endet immer mit einer erfolgreichen Anwendung des Matchers. Daher ist es grundsätzlich nicht notwendig, den ID-Match nochmal zu prüfen, bevor man in die rekursive Mutation einsteigt. +

+

+ +

+

+ Wie kommt nun aber dieser explizit ausprogrammierte ID-Match in die Standard-Implementierung? Die Antwort ist, letztlich aus Verlegenheit. Denn zunächst einmal hatte ich für den Unit-Test alles für ein sehr spezielles Setup ausprogrammiert. Das ist auch gut so, denn dieses Setup deckt auch Grenzfälle mit ab. Also im Besonderen auch Bindings, die sich sehr speziell in Implementierungsstrukturen einklinken, und eben grade nicht direkt an ein DiffMutable-Subobjekt delegieren. Und diese Offenheit ist der essentielle Grund, warum ich auf ein Diff-Framework setze, und nicht auf ein Datenmodell mit festen Interfaces. Diese Offenheit bedingt aber auch, daß man dem Client die letztendliche Übersetzung der IDs überlassen muß. Für die rekursive Kind-Mutation muß der Client sich ggfs ein internes Implementierungs-Objekt anhand der gegebenen ID heraussuchen. +

+

+ +

+

+ Im einfachen Standardfall jedoch ist das nicht notwendig, denn in diesem häufigsten Standardfall haben wir mehr oder weniger direkt das Objekt aus der Collection in der Hand, auf welches dann der rekursive Mutator angewendet werden soll. Also stand ich beim ersten Schreiben einer Implementierung für diesen einfachen Standardfall vor dem Paradoxon, daß die Funktion einen ID-Parameter bekommt, den man anscheinend hier gar nicht braucht. Und, ohne diese Zusammenhänge damals zu verstehen, habe ich dann aus Verlegenheit noch eine Zeile Code eingebaut, die "was Sinnvolles mit dieser ID macht", denn es war ja zunächst auch erst mal ein Proof-of-Concept (und zu diesem Zeitpunkt gab es noch zwei weitere, alternative Implementierungen des TreeMutator-Interfaces). Letztlich hat sich dann dieses "anstandshalber" eingebaute Zeile, eben genau der ID-match, welcher mithin "etwas Sinnvolles" mit der ID macht, per Copy-n-Paste in alle konkreten Implementierungen fortgepflanzt. Wiewohl dieser Schritt im Stand der gegenwärtigen Implementierung stets redundant ist. +

+ +
+
- + + + + + + + + + + +

+ ...falls die clientseitige Datenstruktur einen Index verwendet, um per ID die eigentliche Zieldatenstruktur zu betreten. +

+ +
+
+
+
+
+
+
+
+