From 4348e110cb080ae8f1b66ca077b6f6578810959d Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Wed, 27 Sep 2023 01:27:53 +0200 Subject: [PATCH] =?UTF-8?q?Library:=20change=20of=20plan=20-=20retain=20th?= =?UTF-8?q?e=20=C2=BBEither=C2=AB=20wrapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit on second thought, the ability to transport an exception still seems worthwhile, and can be achieved by some rearrangements in the design. As preparation, reorganise the design of the Either-wrapper (lib::Result) --- src/lib/result.hpp | 137 +++++++++--------- tests/15library.tests | 5 + tests/library/result-test.cpp | 108 ++++++++++++++ wiki/thinkPad.ichthyo.mm | 259 ++++++++++++++++++++++++++++++---- 4 files changed, 416 insertions(+), 93 deletions(-) create mode 100644 tests/library/result-test.cpp diff --git a/src/lib/result.hpp b/src/lib/result.hpp index 513340dc8..c7dcd122b 100644 --- a/src/lib/result.hpp +++ b/src/lib/result.hpp @@ -52,74 +52,23 @@ namespace lib { using util::isnil; using std::string; namespace error = lumiera::error; - - + /** - * Optional Result value or status of some operation. - * It can be created for passing a result produced by the operation, or the - * failure to do so. The value can be retrieved by implicit or explicit conversion. + * Representation of the result of some operation, _EITHER_ a value or a failure. + * It can be created for passing a result produced by the operation, or the failure + * to do so. The value can be retrieved by implicit or explicit conversion. * @throw error::State on any attempt to access the value in case of failure * @warning this class has a lot of implicit conversions; * care should be taken when defining functions * to take Result instances as parameter.... */ template - class Result - { - string failureLog_; - wrapper::ItemWrapper value_; - - public: - /** mark an invalid/failed result */ - Result (string const& failureReason ="no result") - : failureLog_{failureReason} - { } - - /** failed result, with reason given.*/ - Result (lumiera::Error const& reason) - : failureLog_{reason.what()} - { } - - /** standard case: valid result */ - Result (RES&& value) - : failureLog_{} - , value_{std::forward (value)} - { } - - - - bool - isValid() const - { - return value_.isValid(); - } - - void - maybeThrow() const - { - if (not isValid()) - throw error::State (failureLog_, lumiera_error_peek()); - } - - operator RES() const - { - maybeThrow(); - return *value_; - } - - template - TY - get() const - { - maybeThrow(); - return static_cast (*value_); - } - }; + class Result; /** - * Specialisation for signalling success or failure, + * The base case is just to capture success or failure, * without returning any value result. */ template<> @@ -138,23 +87,81 @@ namespace lib { : failureLog_{reason.what()} { } - - - bool - isValid() const - { - return isnil (failureLog_); - } + operator bool() const { return isValid(); } + bool isValid() const { return isnil (failureLog_); } void maybeThrow() const { if (not isValid()) - throw error::State (failureLog_, lumiera_error_peek()); + throw error::State (failureLog_); } }; + /** + * Optional Result value or status of some operation. + * It can be created for passing a result produced by the operation, or the + * failure to do so. The value can be retrieved by implicit or explicit conversion. + * @throw error::State on any attempt to access the value in case of failure + * @warning this class has a lot of implicit conversions; + * care should be taken when defining functions + * to take Result instances as parameter.... + */ + template + class Result + : public Result + { + wrapper::ItemWrapper value_; + + public: + /** mark an invalid/failed result */ + Result (string const& failureReason ="no result") + : Result{failureReason} + { } + + /** failed result, with reason given.*/ + Result (lumiera::Error const& reason) + : Result{reason} + { } + + /** standard case: valid result */ + Result (RES&& value) + : Result{true} + , value_{std::forward (value)} + { } + + + + operator RES() const + { + maybeThrow(); + return *value_; + } + + template + TY + get() const + { + maybeThrow(); + return static_cast (*value_); + } + + template + RES + getOrElse (MAKE&& producer, ARGS ...args) + { + if (isValid()) + return *value_; + else + return std::invoke(std::forward (producer), std::forward (args)...); + } + }; + + template + Result (VAL&& x) -> Result; + + } // namespace lib diff --git a/tests/15library.tests b/tests/15library.tests index 0757347c8..992eac0c4 100644 --- a/tests/15library.tests +++ b/tests/15library.tests @@ -533,6 +533,11 @@ return: 0 END +TEST "Either-result-or-failure" Result_test <... out: checking ScopedPtrHolder... diff --git a/tests/library/result-test.cpp b/tests/library/result-test.cpp new file mode 100644 index 000000000..da9c0b441 --- /dev/null +++ b/tests/library/result-test.cpp @@ -0,0 +1,108 @@ +/* + Result(Test) - verify the either-result-or-failure intermediary wrapper + + Copyright (C) Lumiera.org + 2023, 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 result-test.cpp + ** unit test \ref Result_test + */ + + + + +#include "lib/test/run.hpp" +#include "lib/test/test-helper.hpp" +#include "lib/result.hpp" +#include "lib/util.hpp" + +#include + + + +namespace lib { +namespace test{ + + using util::isSameObject; + using std::rand; + + namespace error = lumiera::error; + using error::LUMIERA_ERROR_FATAL; + using error::LUMIERA_ERROR_STATE; ///////////////////TODO + + + namespace { + const Literal THE_END = "all dead and hero got the girl"; + + + #define Type(_EXPR_) lib::test::showType() + } + + + + + + /***********************************************************************************//** + * @test Verify an intermediary »Either« type, to embody either a successful result, + * or document a failure with encountered exception. + * @see result.hpp + * @see lib::ThreadJoinable usage example + */ + class Result_test + : public Test + { + + void + run (Arg) + { + auto happy = Result{THE_END}; + CHECK (happy == THE_END); + CHECK (happy.isValid()); + CHECK (true == happy); + + happy.maybeThrow(); // still alive... + + CHECK (Type(happy) == "Result"_expect); + + // Note type deduction: RValue moved into the Result... + auto sequel = Result{Literal{THE_END}}; + CHECK (sequel.isValid()); + CHECK (Type(sequel) == "Result"_expect); + + CHECK ( isSameObject (happy.get(), THE_END)); + CHECK (not isSameObject (sequel.get(), THE_END)); + + // »Either Right« case : mark as failure + Result facepalm{error::Fatal()}; + CHECK (not facepalm.isValid()); + + VERIFY_ERROR (STATE, (double)facepalm ); + VERIFY_ERROR (STATE, facepalm.get()); + VERIFY_ERROR (STATE, facepalm.maybeThrow() ); + + CHECK (42.0 == facepalm.getOrElse([]{ return 42; })); + } + }; + + + /** Register this test class... */ + LAUNCHER (Result_test, "unit common"); + + +}} // namespace lib::test diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 3f36033b3..ed26b55db 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -78947,6 +78947,21 @@ Date:   Thu Apr 20 18:53:17 2023 +0200

+ + + + + + + +

+ Um das klar zu sagen: zwar hat Christian ein Verbot von »join« angedacht, aber grade in dieser Frage war und bin ich absolut einer Meinung mit ihm. Tatsächlich war Christian sogar zurückhaltender („man sollte es deprecaten“) — und das hat mich  dann auf die Idee gebracht, einen separaten »Joinable«-Wrapper zu schreiben. Und jetzt, viele Jahre später stehe ich an dem Punkt, daß es kein absehbares Usage-Pattern gibt, und mein Design von damals ungerchtfertigt erscheint. +

+ +
+ + +
@@ -79410,17 +79425,6 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - - - - - - - - - @@ -79433,8 +79437,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
...da die Implementierung mit einem aktiven Threadpool verbunden war, gab es dort auch eine Managment-Datenstruktur; die Threadpool-Logik hat eine eventuell im Thread noch erkannte Fehlerflag dorthin gespeichert — und join() konnte diese Fehlerflag dann einfach abholen.

- - +
@@ -79446,8 +79449,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
...C kennt ja keine Exceptions — und der Author des Threadpool-Frameworks hielt Exceptions für ein bestenfalls überflüssiges Feature, das es ganz bestimmt nicht rechtfertigt, zusätzlichen Aufwand zu treiben

- -
+
@@ -79459,8 +79461,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
gemeint ist lib::Result

- -
+
@@ -79476,8 +79477,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
...und zwar im Besonderen, wenn man in einer »reaping-loop« alle Kind-Threads ernten möchte; dann würde nur ein Fehler ausgeworfen, und die weiteren Kinder bleiben ungeerntet (und terminieren jetzt, mit der C++14-Lösung sogar das Programm)

- - +
@@ -79504,9 +79504,10 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - + + + + @@ -79517,8 +79518,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
Das ist ein klassischer Fall für ein Feature, das zu Beginn so offensichtlich und irre nützlich aussieht — dann aber in der Realität nicht wirklich „fliegt“

- -
+
@@ -79530,8 +79530,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
heute bevorzugt man für ähnliche Anforderungen das Future / Promise - Konstrukt

- -
+
@@ -79544,13 +79543,217 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
Denn tatsächlich sind aufgetretene Fehler dann ehr schon eine Form von Zustand, den man mit einem speziellen Protokoll im Thread-Objekt erfassen und nach dem join() abfragen sollte; so kann man auch die Ergebnisse mehrerer Threads korrekt kombinieren

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

+ Knoten durchhauen! +

+

+ Warum quäle ich mich damit herum, was ich künftig dürfen darf, und was unangemessen wäre? Meine Einstellung ist doch, daß eine Library strukturieren sollte, aber nicht vorgreifen und bestimmen. Wenn die letzten 10 Jahre eines gezeigt haben, dann den Umstand, daß der Thread-Wrapper vor allem für Tests verwendet wird. Und genau da gelten die ganzen Performance- und Architektur-Überlegungen überhaupt nicht; sondern es kommt auf die den Umständen entsprechende, gradlinige Formulierung im Testcode an. Und da könnte unter Umständen Fork-Join genau die KISS-Lösung sein. Insofern wäre das Design bereits nahezu gelungen: es trennt einen intendierten Standardfall ab, läßt aber sonst alle Türen offen.... +

+ +
+ + + + +
+ + + + +
+
+ + + + + + + + + + + + + + + + +

+ und kann zugleich jedwede Exception halten +

+ +
+
+
+ + + + + + + +

+ und ansonsten aber left-biassed sein +

+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + +

+ Es wäre zwar so irgendwie hinzubekommen — aber weit entfernt von einem guten Design. +

+

+ Ich baue hier eine auf Dauer angelegte Lösung, die auch sichtbar ist und in Frage gestellt werden wird! +

+ + +
+ + + + + + + +

+ hab das gestern versucht — so kann das nicht stehen bleiben, das ist ja lächerlich (brauche explizite Template-Instantiierung) — zumindest solange wir keine C++-Module verwenden (C++20) +

+ + +
+
+ + + + + + +

+ die Code-Anordnung hat keinen Flow +

+ + +
+ +
+ + + + + + +

+ Ich habe doch selber oben diese Prinzipien formuliert: eine Library-Lösung sollte nicht mutwillig beschränken, sondern eine sinnvolle Gliederung anbieten... +

+ + +
+ + +
+
+ + + + + + +

+ also vielleicht doch Policy-based-Design? +

+ + +
+ + + + + + + + + + + + + +

+ ⟹ wenn überhaupt, müßte die Policy einen mittleren Binding-Layer bilden +

+ + +
+
+ + + + + + + + + + + + + + + + + + + +

+ Policy  main<POL,FUN,ARGS...> +

+ + +
+
+
+ +