Terminology

Futures

A future is a container that will eventually contain a result or an error.

Programmers often use the words “future” and “promise” interchangeably. Libdex tries, when possible, to follow the academic nomenclature for futures. That is to say that a future is the interface and promise is a type of future.

Futures exist in one of three states. The first state is DEX_FUTURE_STATUS_PENDING. A future exists in this state until it has either rejected or resolved.

The second state is DEX_FUTURE_STATUS_RESOLVED. A future reaches this state when it has successfully obtained a value.

The last third state is DEX_FUTURE_STATUS_REJECTED. If there was a failure to obtain a value a future will be in this state and contain a GError representing such failure.

Promises and More

A promise is a type of future that allows the creator to set the resolved value or error. This is a common type of future to use when you are integrating with things that are not yet integrated with Libdex.

Other types of futures also exist.

/* resolve to "true" */
DexPromise *good = dex_promise_new ();
dex_promise_resolve_boolean (good, TRUE);

/* reject with error */
DexPromise *bad = dex_promise_new ();
dex_promise_reject (good,
                    g_error_new (G_IO_ERROR,
                                 G_IO_ERROR_FAILED,
                                 "Failed"));

Static Futures

Sometimes you already know the result of a future upfront. The DexStaticFuture is used in this case. Various constructors for DexFuture will help you create one.

For example, dex_future_new_take_object() will create a static future for a GObject-derived instance.

DexFuture *future = dex_future_new_for_int (123);

Blocks

One of the most commonly used types of futures in Libdex is a DexBlock.

a DexBlock is a callback that is run to process the result of a future. The block itself is also a future meaning that you can chain these blocks together into robust processing groups.

"Then" Blocks

The first type of block is a “then” block which is created using dex_future_then(). These blocks will only be run if the dependent future resolves with a value. Otherwise, the rejection of the dependent future is propagated to the block.

static DexFuture *
further_processing (DexFuture *future,
                    gpointer   user_data)
{
  const GValue *result = dex_promise_get_value (future, NULL);

  return dex_ref (future);
}

"Catch" Blocks

Since some futures may fail, there is value in being able to “catch” the failure and resolve it. Use dex_future_catch() to handle the result of a rejected future and resolve or reject it further.

static DexFuture *
catch_rejection (DexFuture *future,
                 gpointer   user_data)
{
  g_autoptr(GError) error = NULL;

  dex_future_get_value (future, &error);

  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
    return dex_future_new_true ();

  return dex_ref (future);
}

"Finally" Blocks

There may be times when you want to handle completion of a future whether it resolved or rejected. For this case, use a “finally” block by calling dex_future_finally().

Infinite Loops

If you find you have a case where you want a DexBlock to loop indefinitely, you can use the “_loop” variants of the block APIs.

See dex_future_then_loop(), dex_future_catch_loop(), or dex_future_finally_loop(). This is generally useful when your block’s callback will begin the next stage of work as the result of the callback.

Future Sets

A DexFutureSet is a type of future that contains the result of multiple futures. This is an extremely useful construct because it allows you to do work concurrently and then process the results in a sort of “reduce” phase.

For example, you could make a request to a database, cache server, and a timeout and process the first that completes.

There are multiple types of future sets based on the type of problem you want to solve.

dex_future_all() can be used to resolve when all dependent futures have resolved, otherwise it will reject with error once they are complete. If you want to reject as soon as the first item rejects, dex_future_all_race() will get you that behavior.

Other useful DexFutureSet constructors include dex_future_any() and dex_future_first().

/* Either timeout or propagate result of cache/db query */
return dex_future_first (dex_timeout_new_seconds (60),
                         dex_future_any (query_db_server (),
                                         query_cache_server (),
                                         NULL),
                         NULL);

Cancellable

Many programmers who use GTK and GIO are familiar with GCancellable. Libdex has something similar in the form of DexCancellable. However, in the Libdex case, DexCancellable is a future.

It allows for convenient grouping with other futures to perform cancellation when the dex_cancellable_cancel() method is called.

It can also integrate with GCancellable when created using dex_cancellable_new_from_cancellable().

A DexCancellable will only ever reject.

DexFuture *future = dex_cancellable_new ();

dex_cancellable_cancel (DEX_CANCELLABLE (future));

Timeouts

A timeout may be represented as a future. In this case, the timeout will reject after a time period has passed.

A DexTimeout will only ever reject.

This future is implemented on top of GMainContext via API like g_timeout_add().

DexFuture *future = dex_timeout_new_seconds (60);

Unix Signals

Libdex can represent unix signals as a future. That is to say that the future will resolve to an integer of the signal number when that signal has been raised.

This is implemented using [func@GLibUnix@signal_source_new] and comes with all the same restrictions.

Delayed

Sometimes you may run into a case where you want to gate the result of a future until a specific moment.

For this case, DexDelayed allows you to wrap another future and decide when to “uncork” the result.

DexFuture *delayed = dex_delayed_new (dex_future_new_true ());

dex_delayed_release (DEX_DELAYED (delayed));

Fibers

Another type of future is a “fiber”.

More care will be spent on fibers later on but suffice to say that the result of a fiber is easily consumable as a future via DexFiber.

DexFuture *future = dex_scheduler_spawn (NULL, 0, my_fiber, state, state_free);

Cancellation Propagation

Futures within your application will inevitably depend on other futures.

If all of the futures depending on a future have been released, the dependent future will have the opportunity to cancel itself. This allows for cascading cancellation so that unnecessary work may be elided.

You can use dex_future_disown() to ensure that a future will continue to be run even if the dependent futures are released.

Schedulers

Libdex requires much processing that needs to be done on the main loop of a thread. This is generally handled by a DexScheduler.

The main thread of an application has the default scheduler which is a DexMainScheduler.

Libdex also has a managed thread pool of schedulers via the DexThreadPoolScheduler.

Schedulers manage short tasks, executing DexBlock when they are ready, finalizing objects on their owning thread, and running fibers.

Schedulers integrate with the current threads GMainContext via GSource making it easy to use Libdex with GTK and Clutter-based applications.

Channels

Channels are a higher-level construct built on futures that allow passing work between producers and consumers. They are akin to Go channels in that they have a read and a write side. However, they are much more focused on integrating well with DexFuture.