Page MenuHomePhabricator

Promise
Updated 905 Days AgoPublic

Introduction

This is my (@tasn) take on promises. This is very much based and evolved on all of the great work by @felipealmeida.

My previous proposal was again misguided. I only took into account one side of the table, which is the pro-promise camp, without consulting with the anti-promise camp. This proposal should hopefully satisfy both camps. It leverages the power of promises as async value stores when makes sense without cluttering our API when it doesn't.

The proposal

In this proposal I'm differentiating between two concepts. "Setters" and "getters". Setters are calls that set a value on an object, for example: efl_file_set(). Getters are calls that get value from on object, for example: efl_download(), which downloads a file.

Let me first focus on the getters. They are the really interesting ones, I will deal with setters later.

Getters

In pseudo, javascript like, code this is how we will be doing an sql query:

con = Sqlite.Connection('bla.db');
// ...
q = con.query('SELECT * FROM table');
q.then((rows, more_data, event_more_data) => { // do something with the rows
});

This is exactly when you did with promises. You have a promise returned from an API and the C equivalent looks like this. You set a value on the promise, and then you get it back. The value in most cases is another complex object (value? eo? struct?) that wraps around the value. In this example we will probably return an iterator of rows. This is OK, but it means that first of all, we have a generic Promise construct that needs to hold a value, so we need to add things like free callbacks for the value, think about the lifetime of the value, and the containing promise, and a lot of things that are not really natural uses of promises.

In addition, this design led to annoying things like implicit double-casting in C (losing safety), and no real safety in other languages either (what happens if we set a different type in C than we declare in the interface?). Not to mention, this has lead to an annoying syntax in Eolian that doesn't really make sense.

Let me thus change the pseudo code above to how it will look after the changes

con = Sqlite.Connection('bla.db');
// ...
q = con.query('SELECT * FROM table');
q.then((value) => {
    rows = value.rows_get();
    extra_data = value.extra_data_get(); // if needed
    // Do something
});

I'm not even sure we will want to pass value, maybe just for familiarity to JS programmers we will do it in JS. However, the point is, the async object itself (our promise equivalent) will be holding all of the information needed. Because the type is known and it's declared in Eolian, all of its properties, as many as we want, and always extensible without breaking API will be available.

Code

This is an example implementation for Efl.Sql. Obviously it's very bare, it's to emphasise the relevant code path. The Efl.Async class however is complete.

efl_async.eo
 import eina_types;

struct Efl.Async.Info.Progress {
    [[Event info for the progress callback]]
    error: Eina_Error; [[Error reason]]
}

struct Efl.Async.Info.Fail {
    [[Event info for the fail callback]]
    error: Eina_Error; [[Error reason]]
    cancel: bool; [[$true if failed because of cancellation]]
}

class Efl.Async (Efl.Loop_User)
{
   methods {
        register {
             [[Register the callbacks. They could be registered manually by doing
               callback add individually, but this is easier and less error-prone

               Users are allowed to later manilpulate callbacks individually with
               the callback functions

               Callback are only invoked on the next loop iteration! Even if value
               is known...]]
             params {
                 @in success: Efl_Event_Cb; [[Suuccess callback]]
                 @in fail: Efl_Event_Cb; [[Fail (cancel too) callback]]
                 @in user_data: const(void_ptr); [[User data]]
             }
        }
        @property progress {
             [[The current progress of the async task]]
             @protected set {}
             get {}
             values {
                 progress: double;
             }
        }

        cancel {
             [[Cancel the promise]]
        }
        @protected fail {
             [[Mark the promise as failed

              Callbacks only invoked on the next loop iteration!]]
             reason: Eina_Error;
        }
        @protected success {
             [[Mark the promise as success

              Not setting a value, because the value should be held in the object
              itself. You are supposed to inherit from it to add functions to
              query the value. This drastically improves the lifetime handling
              of the value

              Callbacks only invoked on the next loop iteration!]]
        }

        @class race { [[Need to decide on the best way to pass multiple values. ]] }}
        @class all { [[Need to decide on the best way to pass multiple values. ]] }}

        // Maybe:
        chain_register {
             [[I don't think it's necessary anymore, but we can add it if needed.

                Maybe just a spceial callback...]]
             params {
                 @in success: Efl_Event_Cb; [[Suuccess callback]]
                 @in fail: Efl_Event_Cb; [[Fail (cancel too) callback]]
                 @in user_data: const(void_ptr); [[User data]]
             }
        }
   }
   events {
        sucess; [[Called on success]]
        fail: Efl.Async.Info.Fail; [[Called on success]]
        progress: Efl.Async.Info.Progress; [[A callback called on progress done on the object.]]
   }
}
efl_sql and efl_sql_response
import eina_types;

struct Efl.Sql.Row {
     // ...
}

// Heavily stripped version of an sql connection
class Efl.Sql (Efl.Object)
{
   methods {
        query {
             [[Really stupid implementation]]
             params {
                 @in query: string; [[Query string]]
             }
             return Efl.Sql.Query @warn_unused;
        }
	connect { // ...
        }
        // ...
   }
}

// Same file so it's easier
class Efl.Sql.Query (Efl.Async)
{
   methods {
        @property rows {
             [[Really stupid implementation]]
             get { }
             values {
                rows: iterator<Efl.Sql.Row>;
             }
        }
        // ...
   }
}
User code
static void
_query_execute(void)
{
    Efl_Async *race, *all;
    Efl_Sql *sql = efl_add(EFL_SQL_CLASS, NULL, efl_sql_connect('bla.db'));
    Efl_Sql_Response *q1, *q2;
    q1 = efl_sql_query("SELECT * FROM table;");
    q2 = efl_sql_query("SELECT a, b FROM table;");
    efl_async_register(q1, _select1_success, _select1_fail, user_data);
    efl_async_register(q2, _select2_success, _select2_fail, user_data);

    // I wana know about the sucess before everyone else!
    efl_event_callback_priority_add(q1, EFL_ASYNC_SUCCESS, -1000, _success1_first, user_data);

    race = efl_async_race(EFL_ASYNC_CLASS, q1, q2); // Let's see which is fastest
    all = efl_async_all(EFL_ASYNC_CLASS, q1, q2);

    efl_async_register(race, _race_success, _race_fail, user_data);
    efl_async_register(all, _all_success, _all_fail, user_data);

    // We can use the promises as much as we like in this context, but not after
    // if we want to keep after, we should ref!

    pd->q2 = efl_ref(q2); // save for later
}

static void
_select1_success(void *data, const Efl_Event *event)
{
   Private_Data *pd = data;
   EINA_ITERATOR_FOREACH(efl_sql_query_rows_get(event->object), ...) {
        // Do something with the rows
   }

   // I also want to keep the values for later!
   pd->query = efl_ref(event->object);
}

Setters

Setters remain like old-school efl, at least for now. This means that for efl_file_set(),
we will just emit a normal event on the object, no promise or anything. There is no chance
for a clash here because there's only one file per object, so it's really easy to manage.

I could be open to just doing the implicit automatic magic cleaning of promises that are
not handled, however, I think performance will be an issue here if we really start using it.
We will easily exhaust our pools/trash/recycling (think of a genlist load or maybe a resize),
and I don't see the point.

Let's focus on the getters case, because if we agree on that, the same could apply for setters.
So one step at a time.

Summary (TBD)

Main points:

  • No special treatment in Eolian, yes in bindings. Can be mapped to "native" language promises.
  • Eo is not aware of promises.
  • Will reside in Ecore.
  • Calls are deferred to the main loop iteration (need to watch out of ecore_loop_iterate though!)
  • Inheriting Efl.Async is allowed and encouraged.
  • All/race don't cancel all of the promises on the failure of one by default, it'll be a parameter or something.
  • They are essentially "asynchronous value storage" - matches what raster wanted (and I agree)
  • No efl_async_link - It doesn't make sense to me, as the promise object lives by its own, it doesn't need the object, so why delete automatically (other than parent). Maybe there's a use-case I'm not aware of.

We could also have a generic value type which is essentially what promises are now. The syntax in Eolian will have to slightly support promises to support that. This is useful for int/double/string/iterator/list. For more robust types, like the sql example, it's better to have a type for safety in C and flexibility.

Last Author
tasn
Last Edited
Sep 30 2016, 9:56 AM
Projects
None
Subscribers
jpeg, tasn, felipealmeida