From e3f0f054af122655c56145dc134e5a37285bc6ec Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Mon, 25 Feb 2019 10:07:06 +0100 Subject: [PATCH] Implement QPromise>::reduce(reducer, initialValue) Iterates over all the promise values (i.e. `Sequence`) and reduces the sequence to a single value using the given `reducer` function and an optional `initialValue`. Also provide a static helper to directly reduce values (`QtPromise::reduce(values, reducer, initialValue)`). --- docs/.vuepress/config.js | 2 + docs/qtpromise/api-reference.md | 2 + docs/qtpromise/helpers/reduce.md | 48 ++++ docs/qtpromise/qpromise/reduce.md | 58 ++++ docs/qtpromise/qtconcurrent.md | 2 +- src/qtpromise/qpromise.h | 15 +- src/qtpromise/qpromise.inl | 20 ++ src/qtpromise/qpromise_p.h | 11 +- src/qtpromise/qpromisehelpers.h | 59 +++- tests/auto/qtpromise/helpers/helpers.pro | 1 + .../auto/qtpromise/helpers/reduce/reduce.pro | 4 + .../qtpromise/helpers/reduce/tst_reduce.cpp | 271 ++++++++++++++++++ tests/auto/qtpromise/qpromise/qpromise.pro | 1 + .../auto/qtpromise/qpromise/reduce/reduce.pro | 4 + .../qtpromise/qpromise/reduce/tst_reduce.cpp | 271 ++++++++++++++++++ 15 files changed, 763 insertions(+), 6 deletions(-) create mode 100644 docs/qtpromise/helpers/reduce.md create mode 100644 docs/qtpromise/qpromise/reduce.md create mode 100644 tests/auto/qtpromise/helpers/reduce/reduce.pro create mode 100644 tests/auto/qtpromise/helpers/reduce/tst_reduce.cpp create mode 100644 tests/auto/qtpromise/qpromise/reduce/reduce.pro create mode 100644 tests/auto/qtpromise/qpromise/reduce/tst_reduce.cpp diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index a6c1e31..085a4dd 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -33,6 +33,7 @@ module.exports = { 'qtpromise/qpromise/ispending', 'qtpromise/qpromise/isrejected', 'qtpromise/qpromise/map', + 'qtpromise/qpromise/reduce', 'qtpromise/qpromise/tap', 'qtpromise/qpromise/tapfail', 'qtpromise/qpromise/then', @@ -51,6 +52,7 @@ module.exports = { 'qtpromise/helpers/each', 'qtpromise/helpers/filter', 'qtpromise/helpers/map', + 'qtpromise/helpers/reduce', 'qtpromise/helpers/resolve' ] }, diff --git a/docs/qtpromise/api-reference.md b/docs/qtpromise/api-reference.md index 62693ef..d8ae8f2 100644 --- a/docs/qtpromise/api-reference.md +++ b/docs/qtpromise/api-reference.md @@ -12,6 +12,7 @@ * [`QPromise::isPending`](qpromise/ispending.md) * [`QPromise::isRejected`](qpromise/isrejected.md) * [`QPromise::map`](qpromise/map.md) +* [`QPromise::reduce`](qpromise/reduce.md) * [`QPromise::tap`](qpromise/tap.md) * [`QPromise::tapFail`](qpromise/tapfail.md) * [`QPromise::then`](qpromise/then.md) @@ -31,6 +32,7 @@ * [`QtPromise::each`](helpers/each.md) * [`QtPromise::filter`](helpers/filter.md) * [`QtPromise::map`](helpers/map.md) +* [`QtPromise::reduce`](helpers/reduce.md) * [`QtPromise::resolve`](helpers/resolve.md) ## Exceptions diff --git a/docs/qtpromise/helpers/reduce.md b/docs/qtpromise/helpers/reduce.md new file mode 100644 index 0000000..fc81984 --- /dev/null +++ b/docs/qtpromise/helpers/reduce.md @@ -0,0 +1,48 @@ +--- +title: reduce +--- + +# QtPromise::reduce + +*Since: 0.5.0* + +```cpp +QPromise::reduce(Sequence> values, Reducer reducer) -> QPromise +QPromise::reduce(Sequence> values, Reducer reducer, R|QPromise initialValue) -> QPromise + +// With: +// - Sequence: STL compatible container (e.g. QVector, etc.) +// - Reducer: Function(T|R accumulator, T item, int index) -> R|QPromise +``` + +Iterates over `values` and [reduces the sequence to a single value](https://en.wikipedia.org/wiki/Fold_%28higher-order_function%29) using the given `reducer` function and an optional `initialValue`. The type returned by `reducer` determines the type of the `output` promise. If `reducer` throws, `output` is rejected with the new exception. + +If `reducer` returns a promise (or `QFuture`), then the result of the promise is awaited, before continuing with next iteration. If any promise in the `values` sequence is rejected or a promise returned by the `reducer` function is rejected, `output` immediately rejects with the error of the promise that rejected. + +```cpp +// Concatenate the content of the given files, read asynchronously +auto output = QtPromise::reduce(QList{ + "file:f0.txt", // contains "foo" + "file:f1.txt", // contains "bar" + "file:f2.txt" // contains "42" +}, [](const QString& acc, const QString& cur, int idx) { + return readAsync(cur).then([=](const QString& res) { + return QString("%1;%2:%3").arg(acc).arg(idx).arg(res); + }); +}, QString("index:text")); + +// 'output' resolves as soon as all promises returned by +// 'reducer' are fulfilled or at least one is rejected. + +// 'output' type: QPromise +output.then([](const QString& res) { + // res == "index:text;0:foo;1:bar;2:42" + // {...} +}); +``` + +::: warning IMPORTANT +The first time `reducer` is called, if no `initialValue` is provided, `accumulator` will be equal to the first value in the sequence, and `currentValue` to the second one (thus index will be `1`). +::: + +See also: [`QPromise::reduce`](../qpromise/reduce.md) diff --git a/docs/qtpromise/qpromise/reduce.md b/docs/qtpromise/qpromise/reduce.md new file mode 100644 index 0000000..0822db2 --- /dev/null +++ b/docs/qtpromise/qpromise/reduce.md @@ -0,0 +1,58 @@ +--- +title: .reduce +--- + +# QPromise::reduce + +*Since: 0.5.0* + +```cpp +QPromise>::reduce(Reducer reducer) -> QPromise +QPromise>::reduce(Reducer reducer, R|QPromise initialValue) -> QPromise + +// With: +// - Sequence: STL compatible container (e.g. QVector, etc.) +// - Reducer: Function(T|R accumulator, T item, int index) -> R|QPromise +``` + +::: warning IMPORTANT +This method only applies to promise with sequence value. +::: + +Iterates over all the promise values (i.e. `Sequence`) and [reduces the sequence to a single value](https://en.wikipedia.org/wiki/Fold_%28higher-order_function%29) using the given `reducer` function and an optional `initialValue`. The type returned by `reducer` determines the type of the `output` promise. + +See [`QtPromise::reduce`](../helpers/reduce.md) for details, this method is provided for convenience and is similar to: + +```cpp +promise.then([](const T& values) { + return QtPromise::reduce(values, reducer, initialValue); +}) +``` + +For example: + +```cpp +auto input = QtPromise::resolve(QList{ + "file:f0.txt", // contains "foo" + "file:f1.txt", // contains "bar" + "file:f2.txt" // contains "42" +}); + +// Concatenate the content of the given files, read asynchronously +auto output = input.reduce([](const QString& acc, const QString& cur, int idx) { + return readAsync(cur).then([=](const QString& res) { + return QString("%1;%2:%3").arg(acc).arg(idx).arg(res); + }); +}, QString("index:text")); + +// 'output' resolves as soon as all promises returned by +// 'reducer' are fulfilled or at least one is rejected. + +// 'output' type: QPromise +output.then([](const QString& res) { + // res == "index:text;0:foo;1:bar;2:42" + // {...} +}); +``` + +See also: [`QtPromise::reduce`](../helpers/reduce.md) diff --git a/docs/qtpromise/qtconcurrent.md b/docs/qtpromise/qtconcurrent.md index cef0c3d..50c802e 100644 --- a/docs/qtpromise/qtconcurrent.md +++ b/docs/qtpromise/qtconcurrent.md @@ -46,7 +46,7 @@ The `output` promise is resolved when the `QFuture` is [finished](https://doc.qt ## Error -Exceptions thrown from a QtConcurrent thread reject the associated promise with the exception as the reason. Note that if you throw an exception that is not a subclass of `QException`, the promise with be rejected with [`QUnhandledException`](https://doc.qt.io/qt-5/qunhandledexception.html#details) (this restriction only applies to exceptions thrown from a QtConcurrent thread, [read more](https://doc.qt.io/qt-5/qexception.html#details)). +Exceptions thrown from a QtConcurrent thread reject the associated promise with the exception as the reason. Note that if you throw an exception that is not a subclass of `QException`, the promise will be rejected with [`QUnhandledException`](https://doc.qt.io/qt-5/qunhandledexception.html#details) (this restriction only applies to exceptions thrown from a QtConcurrent thread, [read more](https://doc.qt.io/qt-5/qexception.html#details)). ```cpp QPromise promise = ... diff --git a/src/qtpromise/qpromise.h b/src/qtpromise/qpromise.h index d105b42..653da62 100644 --- a/src/qtpromise/qpromise.h +++ b/src/qtpromise/qpromise.h @@ -76,6 +76,7 @@ public: // STATIC protected: friend struct QtPromisePrivate::PromiseFulfill>; friend class QtPromisePrivate::PromiseResolver; + friend struct QtPromisePrivate::PromiseInspect; QExplicitlySharedDataPointer> m_d; }; @@ -88,15 +89,25 @@ public: QPromise(F&& resolver): QPromiseBase(std::forward(resolver)) { } template - inline QPromise each(Functor fn); + inline QPromise + each(Functor fn); template - inline QPromise filter(Functor fn); + inline QPromise + filter(Functor fn); template inline typename QtPromisePrivate::PromiseMapper::PromiseType map(Functor fn); + template + inline typename QtPromisePrivate::PromiseDeduce::Type + reduce(Functor fn, Input initial); + + template + inline typename QtPromisePrivate::PromiseDeduce::Type + reduce(Functor fn); + public: // STATIC // DEPRECATED (remove at version 1) diff --git a/src/qtpromise/qpromise.inl b/src/qtpromise/qpromise.inl index 3ac7c64..fc1f485 100644 --- a/src/qtpromise/qpromise.inl +++ b/src/qtpromise/qpromise.inl @@ -197,6 +197,26 @@ QPromise::map(Functor fn) }); } +template +template +inline typename QtPromisePrivate::PromiseDeduce::Type +QPromise::reduce(Functor fn, Input initial) +{ + return this->then([=](const T& values) { + return QtPromise::reduce(values, fn, initial); + }); +} + +template +template +inline typename QtPromisePrivate::PromiseDeduce::Type +QPromise::reduce(Functor fn) +{ + return this->then([=](const T& values) { + return QtPromise::reduce(values, fn); + }); +} + template template