Promises/A+ # QtPromise [![Travis](https://img.shields.io/travis/simonbrunel/qtpromise.svg?style=flat-square)](https://travis-ci.org/simonbrunel/qtpromise) [![coverage](https://img.shields.io/codecov/c/github/simonbrunel/qtpromise.svg?style=flat-square)](https://codecov.io/gh/simonbrunel/qtpromise) [Promises/A+](https://promisesaplus.com/) implementation for [Qt/C++](https://www.qt.io/). Requires [Qt 5.4](https://www.qt.io/download/) (or later) with [C++11 support enabled](https://wiki.qt.io/How_to_use_C++11_in_your_Qt_Projects). ## Getting Started ### Installation QtPromise is a [header-only](https://en.wikipedia.org/wiki/Header-only) library, simply download the [latest release](https://github.com/simonbrunel/qtpromise/releases/latest) (or [`git submodule`](https://git-scm.com/docs/git-submodule])) and include `qtpromise.pri` from your project `.pro`. ### Usage The recommended way to use QtPromise is to include the single module header: ```cpp #include ``` ### Example Let's first make the code more readable by using the library namespace: ```cpp using namespace QtPromise; ``` This `download` function creates a [promise from callbacks](#qpromise-qpromise) which will be resolved when the network request is finished: ```cpp QPromise download(const QUrl& url) { return QPromise([&]( const QPromiseResolve& resolve, const QPromiseReject& reject) { QNetworkReply* reply = manager->get(QNetworkRequest(url)); QObject::connect(reply, &QNetworkReply::finished, [=]() { if (reply->error() == QNetworkReply::NoError) { resolve(reply->readAll()); } else { reject(reply->error()); } reply->deleteLater(); }); }); } ``` The following method `uncompress` data in a separate thread and returns a [promise from QFuture](#qtconcurrent): ```cpp QPromise uncompress(const QByteArray& data) { return qPromise(QtConcurrent::run([](const QByteArray& data) { Entries entries; // {...} uncompress data and parse content. if (error) { throw MalformedException(); } return entries; }, data)); } ``` It's then easy to chain the whole asynchronous process using promises: - initiate the promise chain by downloading a specific URL, - [`then`](#qpromise-then) *and only if download succeeded*, uncompress received data, - [`then`](#qpromise-then) validate and process the uncompressed entries, - [`finally`](#qpromise-finally) perform operations whatever the process succeeded or failed, - and hande specific errors using [`fail`](#qpromise-fail). ```cpp download(url).then(&uncompress).then([](const Entries& entries) { if (entries.isEmpty()) { throw UpdateException("No entries"); } // {...} process entries }).finally([]() { // {...} cleanup }).fail([](QNetworkReply::NetworkError err) { // {...} handle network error }).fail([](const UpdateException& err) { // {...} handle update error }).fail([]() { // {...} catch all }); ``` ## QtConcurrent QtPromise integrates with [QtConcurrent](http://doc.qt.io/qt-5/qtconcurrent-index.html) to make easy chaining QFuture with QPromise. ### Convert Converting `QFuture` to `QPromise` is done using the [`qPromise`](#helpers-qpromise) helper: ```cpp QFuture future = QtConcurrent::run([]() { // {...} return 42; }); QPromise promise = qPromise(future); ``` or simply: ```cpp auto promise = qPromise(QtConcurrent::run([]() { // {...} })); ``` ### Chain Returning a `QFuture` in [`then`](#qpromise-then) or [`fail`](#qpromise-fail) automatically translate to `QPromise`: ```cpp QPromise input = ... auto output = input.then([](int res) { return QtConcurrent::run([]() { // {...} return QString("42"); }); }); // output type: QPromise output.then([](const QString& res) { // {...} }); ``` The `output` promise is resolved when the `QFuture` is [finished](http://doc.qt.io/qt-5/qfuture.html#isFinished). ### 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`](http://doc.qt.io/qt-5/qunhandledexception.html#details) (this restriction only applies to exceptions thrown from a QtConcurrent thread, [read more](http://doc.qt.io/qt-5/qexception.html#details)). ```cpp QPromise promise = ... promise.then([](int res) { return QtConcurrent::run([]() { // {...} if (!success) { throw CustomException(); } return QString("42"); }); }).fail(const CustomException& err) { // {...} }); ``` ## Thread-Safety QPromise is thread-safe and can be copied and accessed across different threads. QPromise relies on [explicitly data sharing](http://doc.qt.io/qt-5/qexplicitlyshareddatapointer.html#details) and thus `auto p2 = p1` represents the same promise: when `p1` resolves, handlers registered on `p1` and `p2` are called, the fulfilled value being shared between both instances. > **Note:** while it's safe to access the resolved value from different threads using [`then`](#qpromise-then), QPromise provides no guarantee about the object being pointed to. Thread-safety and reentrancy rules for that object still apply. ## QPromise ### `QPromise::QPromise(resolver)` Creates a new promise that will be fulfilled or rejected by the given `resolver` lambda: ```cpp QPromise promise([](const QPromiseResolve& resolve, const QPromiseReject& reject) { async_method([=](bool success, int result) { if (success) { resolve(result); } else { reject(customException()); } }); }); ``` > **Note:** `QPromise` is specialized to not contain any value, meaning that the `resolve` callback takes no argument. **C++14** ```cpp QPromise promise([](const auto& resolve, const auto& reject) { // {...} }); ``` ### `QPromise::then(onFulfilled, onRejected) -> QPromise` See [Promises/A+ `.then`](https://promisesaplus.com/#the-then-method) for details. ```cpp QPromise input = ... auto output = input.then([](int res) { // called with the 'input' result if fulfilled }, [](const ReasonType& reason) { // called with the 'input' reason if rejected // see QPromise::fail for details }); ``` > **Note**: `onRejected` handler is optional, `output` will be rejected with the same reason as `input`. > **Note**: it's recommended to use the [`fail`](#qpromise-fail) shorthand to handle errors. The type `` of the `output` promise depends on the return type of the `onFulfilled` handler: ```cpp QPromise input = {...} auto output = input.then([](int res) { return QString::number(res); // -> QPromise }); // output type: QPromise output.then([](const QString& res) { // {...} }); ``` > **Note**: only `onFulfilled` can change the promise type, `onRejected` **must** return the same type as `onFulfilled`. That also means if `onFulfilled` is `nullptr`, `onRejected` must return the same type as the `input` promise. ```cpp QPromise input = ... auto output = input.then([](int res) { return res + 4; }, [](const ReasonType& reason) { return -1; }); ``` If `onFulfilled` doesn't return any value, the `output` type is `QPromise`: ```cpp QPromise input = ... auto output = input.then([](int res) { // {...} }); // output type: QPromise output.then([]() { // `QPromise` `onFulfilled` handler has no argument }); ``` You can also decide to skip the promise result by omitting the handler argument: ```cpp QPromise input = {...} auto output = input.then([]( /* skip int result */ ) { // {...} }); ``` The `output` promise can be *rejected* by throwing an exception in either `onFulfilled` or `onRejected`: ```cpp QPromise input = {...} auto output = input.then([](int res) { if (res == -1) { throw ReasonType(); } else { return res; } }); // output.isRejected() is true ``` If an handler returns a promise (or QFuture), the `output` promise is delayed and will be resolved by the returned promise. ### `QPromise::fail(onRejected) -> QPromise` Shorthand to `promise.then(nullptr, onRejected)`, similar to the [`catch` statement](http://en.cppreference.com/w/cpp/language/try_catch): ```cpp promise.fail([](const MyException&) { // {...} }).fail(const QException&) { // {...} }).fail(const std::exception&) { // {...} }).fail() { // {...} catch-all }); ``` ### `QPromise::finally(handler) -> QPromise` This `handler` is **always** called, without any argument and whatever the `input` promise state (fulfilled or rejected). The `output` promise has the same type as the `input` one but also the same value or error. The finally `handler` **can not modify the fulfilled value** (the returned value is ignored), however, if `handler` throws, `output` is rejected with the new exception. ```cpp auto output = input.finally([]() { // {...} }); ``` If `handler` returns a promise (or QFuture), the `output` promise is delayed until the returned promise is resolved and under the same conditions: the delayed value is ignored, the error transmitted to the `output` promise. ### `QPromise::wait() -> QPromise` This method holds the execution of the remaining code **without** blocking the event loop of the current thread: ```cpp int result = -1; QPromise input = qPromise(QtConcurrent::run([]() { return 42; })); auto output = input.then([&](int res) { result = res; }); // output.isPending() is true && result is -1 output.wait(); // output.isPending() is false && result is 42 ``` ### `QPromise::isPending() -> bool` Returns `true` if the promise is pending (not fulfilled or rejected), otherwise returns `false`. ### `QPromise::isFulfilled() -> bool` Returns `true` if the promise is fulfilled, otherwise returns `false`. ### `QPromise::isRejected() -> bool` Returns `true` if the promise is rejected, otherwise returns `false`. ## QPromise (statics) ### `[static] QPromise::resolve(value) -> QPromise` Creates a `QPromise` that is fulfilled with the given `value` of type `T`: ```cpp QPromise compute(const QString& type) { if (type == "magic") { return QPromise::resolve(42); } return QPromise([](const QPromiseResolve& resolve) { // {...} }); } ``` See also: [`qPromise`](#helpers-qpromise) ### `[static] QPromise::reject(reason) -> QPromise` Creates a `QPromise` that is rejected with the given `reason` of *whatever type*: ```cpp QPromise compute(const QString& type) { if (type == "foobar") { return QPromise::reject(QString("Unknown type: %1").arg(type)); } return QPromise([](const QPromiseResolve& resolve) { // {...} }); } ``` ### `[static] QPromise::all(QVector>) -> QPromise>` Returns a `QPromise>` that fulfills when **all** `promises` of (the same) type `T` have been fulfilled. The `output` value is a vector containing **all** the values of `promises`, in the same order. If any of the given `promises` fail, `output` immediately rejects with the error of the promise that rejected, whether or not the other promises are resolved. ```cpp QVector > promises{ download(QUrl("http://a...")), download(QUrl("http://b...")), download(QUrl("http://c...")) }; auto output = QPromise::all(promises); // output type: QPromise> output.then([](const QVector& res) { // {...} }); ``` See also: [`qPromiseAll`](#helpers-qpromiseall) ## Helpers ### `qPromise(T value) -> QPromise` Similar to the `QPromise::resolve` static method, creates a promise resolved from a given `value` without the extra typing: ```cpp auto promise = qPromise(); // QPromise auto promise = qPromise(42); // QPromise auto promise = qPromise(QString("foo")); // QPromise ``` This method also allows to convert `QFuture` to `QPromise` delayed until the `QFuture` is finished ([read more](#qtconcurrent-convert)). ### `qPromiseAll(QVector promises) -> QPromise>` This method simply calls the appropriated [`QPromise::all`](#qpromise-all) static method based on the given `QVector` type. In some cases, this method is more convenient than the static one since it avoid some extra typing: ```cpp QVector > promises{...} auto output = qPromiseAll(promises); // eq. QPromise::all(promises) ``` ## License QtPromise is available under the [MIT license](LICENSE.md).