qtpromise/README.md
Simon Brunel b47ca0569e Implement QPromise::delay(msec)
This method returns a promise that will be fulfilled with the same value as the `input` promise and after at least `msec` milliseconds. If the `input` promise is rejected, the `output` promise is immediately rejected with the same reason.
2017-08-24 18:28:44 +02:00

15 KiB

Promises/A+

QtPromise

qpm Travis coverage

Promises/A+ implementation for Qt/C++.

Requires Qt 5.4 (or later) with C++11 support enabled.

Getting Started

Installation

QtPromise is a header-only library, simply download the latest release (or git submodule) and include qtpromise.pri from your project .pro.

qpm

Alternatively and only if your project relies on qpm, you can install QtPromise as follow:

qpm install com.github.simonbrunel.qtpromise

Usage

The recommended way to use QtPromise is to include the single module header:

#include <QtPromise>

Example

Let's first make the code more readable by using the library namespace:

using namespace QtPromise;

This download function creates a promise from callbacks which will be resolved when the network request is finished:

QPromise<QByteArray> download(const QUrl& url)
{
    return QPromise<QByteArray>([&](
        const QPromiseResolve<QByteArray>& resolve,
        const QPromiseReject<QByteArray>& 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:

QPromise<Entries> 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 and only if download succeeded, uncompress received data,
  • then validate and process the uncompressed entries,
  • finally perform operations whatever the process succeeded or failed,
  • and handle specific errors using fail.
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 to make easy chaining QFuture with QPromise.

Convert

Converting QFuture<T> to QPromise<T> is done using the qPromise helper:

QFuture<int> future = QtConcurrent::run([]() {
    // {...}
    return 42;
});

QPromise<int> promise = qPromise(future);

or simply:

auto promise = qPromise(QtConcurrent::run([]() {
    // {...}
}));

Chain

Returning a QFuture<T> in then or fail automatically translate to QPromise<T>:

QPromise<int> input = ...
auto output = input.then([](int res) {
    return QtConcurrent::run([]() {
        // {...}
        return QString("42");
    });
});

// output type: QPromise<QString>
output.then([](const QString& res) {
    // {...}
});

The output promise is resolved when the QFuture is finished.

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 (this restriction only applies to exceptions thrown from a QtConcurrent thread, read more).

QPromise<int> 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 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 provides no guarantee about the object being pointed to. Thread-safety and reentrancy rules for that object still apply.

QPromise

QPromise<T>::QPromise(resolver)

Creates a new promise that will be fulfilled or rejected by the given resolver lambda:

QPromise<int> promise([](const QPromiseResolve<int>& resolve, const QPromiseReject<int>& reject) {
    async_method([=](bool success, int result) {
        if (success) {
            resolve(result);
        } else {
            reject(customException());
        }
    });
});

Note: QPromise<void> is specialized to not contain any value, meaning that the resolve callback takes no argument.

C++14

QPromise<int> promise([](const auto& resolve, const auto& reject) {
    // {...}
});

QPromise<T>::then(onFulfilled, onRejected) -> QPromise<R>

See Promises/A+ .then for details.

QPromise<int> 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<T>::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 shorthand to handle errors.

The type <R> of the output promise depends on the return type of the onFulfilled handler:

QPromise<int> input = {...}
auto output = input.then([](int res) {
    return QString::number(res);    // -> QPromise<QString>
});

// output type: QPromise<QString>
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.

QPromise<int> 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<void>:

QPromise<int> input = ...
auto output = input.then([](int res) {
    // {...}
});

// output type: QPromise<void>
output.then([]() {
    // `QPromise<void>` `onFulfilled` handler has no argument
});

You can also decide to skip the promise result by omitting the handler argument:

QPromise<int> input = {...}
auto output = input.then([]( /* skip int result */ ) {
    // {...}
});

The output promise can be rejected by throwing an exception in either onFulfilled or onRejected:

QPromise<int> 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<T>::fail(onRejected) -> QPromise<T>

Shorthand to promise.then(nullptr, onRejected), similar to the catch statement:

promise.fail([](const MyException&) {
    // {...}
}).fail(const QException&) {
    // {...}
}).fail(const std::exception&) {
    // {...}
}).fail() {
    // {...} catch-all
});

QPromise<T>::finally(handler) -> QPromise<T>

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.

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<T>::tap(handler) -> QPromise<T>

This handler allows to observe the value of the input promise, without changing the propagated value. The output promise will be resolved with the same value as the input promise (the handler returned value will be ignored). However, if handler throws, output is rejected with the new exception. Unlike finally, this handler is not called for rejections.

QPromise<int> input = {...}
auto output = input.tap([](int res) {
    log(res);
}).then([](int res) {
    // {...}
});

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<T>::delay(handler) -> QPromise<T>

This method returns a promise that will be fulfilled with the same value as the input promise and after at least msec milliseconds. If the input promise is rejected, the output promise is immediately rejected with the same reason.

QPromise<int> input = {...}
auto output = input.delay(2000).then([](int res) {
    // called 2 seconds after `input` is fulfilled
});

QPromise<T>::wait() -> QPromise<T>

This method holds the execution of the remaining code without blocking the event loop of the current thread:

int result = -1;
QPromise<int> 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<T>::isPending() -> bool

Returns true if the promise is pending (not fulfilled or rejected), otherwise returns false.

QPromise<T>::isFulfilled() -> bool

Returns true if the promise is fulfilled, otherwise returns false.

QPromise<T>::isRejected() -> bool

Returns true if the promise is rejected, otherwise returns false.

QPromise (statics)

[static] QPromise<T>::resolve(value) -> QPromise<T>

Creates a QPromise<T> that is fulfilled with the given value of type T:

QPromise<int> compute(const QString& type)
{
    if (type == "magic") {
        return QPromise<int>::resolve(42);
    }

    return QPromise<int>([](const QPromiseResolve<int>& resolve) {
        // {...}
    });
}

See also: qPromise

[static] QPromise<T>::reject(reason) -> QPromise<T>

Creates a QPromise<T> that is rejected with the given reason of whatever type:

QPromise<int> compute(const QString& type)
{
    if (type == "foobar") {
        return QPromise<int>::reject(QString("Unknown type: %1").arg(type));
    }

    return QPromise<int>([](const QPromiseResolve<int>& resolve) {
        // {...}
    });
}

[static] QPromise<T>::all(QVector<QPromise<T>>) -> QPromise<QVector<T>>

Returns a QPromise<QVector<T>> 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.

QVector<QPromise<QByteArray> > promises{
    download(QUrl("http://a...")),
    download(QUrl("http://b...")),
    download(QUrl("http://c..."))
};

auto output = QPromise<QByteArray>::all(promises);

// output type: QPromise<QVector<QByteArray>>
output.then([](const QVector<QByteArray>& res) {
    // {...}
});

See also: qPromiseAll

Helpers

qPromise(T value) -> QPromise<R>

Similar to the QPromise<T>::resolve static method, creates a promise resolved from a given value without the extra typing:

auto promise = qPromise();                // QPromise<void>
auto promise = qPromise(42);              // QPromise<int>
auto promise = qPromise(QString("foo"));  // QPromise<QString>

This method also allows to convert QFuture<T> to QPromise<T> delayed until the QFuture is finished (read more).

qPromiseAll(QVector<QPromise<T> promises) -> QPromise<QVector<T>>

This method simply calls the appropriated QPromise<T>::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:

QVector<QPromise<QByteArray> > promises{...}

auto output = qPromiseAll(promises);
// eq. QPromise<QByteArray>::all(promises)

License

QtPromise is available under the MIT license.