utils icon indicating copy to clipboard operation
utils copied to clipboard

Added Nette\Utils\Future for lazy value evaluation

Open milo opened this issue 3 years ago • 8 comments

  • new feature
  • doc PR: will if accepted

The Future class is a helper for "building data structures" which I use about 4 years. It is handy for small tasks where ORM is too huge, or where two independent data sources are mixing up (database and database, HTTP API and database, local CSV and HTTP API...).

I used to call it "promise" but it is not a promise pattern. It is closer to "future evaluation strategy" but it is not the same. Maybe there exists the corrent name for this evaluation method but I didn't find it.

An example of usage:

# Fetch all books from database with theirs author & editor person ID.
$books = $db->query('
    SELECT
        title,
        author_id AS author,  -- this is an integer, ID of person
        editor_id AS editor   -- this is an integer, ID of person
    FROM
        book
')->fetchAll();

# Replace all authors & editors ID by corresponding entities.
$futurePersons = new Future(function (array $ids) use ($db) {
    return $db->query('SELECT id, first_name, last_name FROM person')->fetchAssoc('id');
});
$futurePersons->bindArraysKey('author', $books);
$futurePersons->bindArraysKey('editor', $books);
$futurePersons->resolve();

On resolve() call, the resolver (callback from constructor) is called with all required authors and editors ID. So there is no need to iterate over books, collects every ID, fetch them and iterate again to build the desired data structure.

The example with database is a little bit funny (even I'm using it in this way). Some ORM does this task probably better. But this Future class is pretty low level helper. If you compose data structures from heterogeneous sources, like files in filesystem, HTTP API or some other JSON sources, the resulting code is nice, short and clean.

Another use case is scalar to value object translation. For example, translate every e-mail string, to Email object:

$resolver = function (array $keys) {
    $result = [];
    foreach ($keys as $key) {
        $result[$key] = new Email($key);
    }
    return $result;
};

(new Future($resolver))
    ->bindArraysKey('from', $data)
    ->bindArraysKey('to', $data)
    ->resolve();

Or scalar to scalar translation (language translator).

Anyway, the workflow is following:

  1. write resolver
  2. bind variables for future evaluation
  3. resolve (translate)

and API looks like:

# Resolve 'key' and store it into $var
->bind('key', $var) 

# Get $var as a key, resolve it and store it back to $var
->bindVar($var) 

# Get an array of values as keys, and resolve them - like bindVar() for every array item
->bindArrayValues($array)

# The array keys are used as keys to be resolved and stored into array values - like bind('key', $var) for every key/value pair
->bindArrayKeys($array)

# Expects every item of $arrays is an array - do bind('key', $item) for every item
->bindArraysKey('key', $arrays)

 # dtto but expects every item is an object and resolved value is stored into propery
->bindObjectsProperty('key', $arrays)

These are common use cases I hit and examples can be found in attached test.

In general, there is a space for ->bindAssoc('a[]->foo->bar', $structure) but I rarely use it. It could be nice but if required, manual iteration through the $structure and ->bind...() calls work too.

One dark side - it is quite hard to goole future term so maybe different name should be used.

milo avatar Nov 19 '21 14:11 milo

I like it. But it really should have better name. It basically just maps/binds values from data source into. Maybe something like LazyMapper? LazyBinder?

MartinMystikJonas avatar Nov 19 '21 15:11 MartinMystikJonas

Yep, the hardest thing :)

milo avatar Nov 19 '21 15:11 milo

Deferred? Delayed?

diegosardina avatar Nov 19 '21 21:11 diegosardina

I think there's a missing $ids, or am I misunderstanding?

$futurePersons = new Future(function (array $ids) use ($db) {
    return $db->query('SELECT id, first_name, last_name FROM person')->fetchAssoc('id');
});

dg avatar Nov 23 '21 00:11 dg

Yes, it is. Sorry, I wrote it from top of my head. Wrong way to introduce new concept :)

$futurePersons = new Future(function (array $ids) use ($db) {
    return $db->query('SELECT id, first_name, last_name FROM person WHERE id IN %in', $ids)->fetchAssoc('id');
});

milo avatar Nov 23 '21 07:11 milo

As for the name, IMHO it has nothing to do with the future, deferring or lazy loading. Rather, it is a bulk replacement of values in various structures, where the important thing is that the all values are first collected and then the replacements are generated, right?

dg avatar Nov 23 '21 15:11 dg

Actually yes.

One note from a class history - I used to call it Promise. It was the very first idea I got and I didn't want to waste the time so I used it. And the higher level code looked like:

class PersonsRepository
{
    public function promisePersonsById(): Promise
    ...

    public function promisePersonsByUsername(): Promise
    ...
}

# and in an another service code
$promisePersonById = $this->personsRepo->promisePersonsById();

# or directly
$this->personsRepo->promisePersonsById()->bind(...)->resolve();

Or usage as a DI service

final class PersonByIdPromise extends Promise
{
    public function __construct(private PersonsRepository $personsRepo)
    {
        parent::__construct(fn(array $ids) => $this->personsRepo->findByIds($ids));
    }
}


final class PersonPromiseFactory
{
    public function byId(): PersonByIdPromise
    ...

    public function byUsername(): PersonByUsernamePromise
    ...
}

Promise is not a right name due to the promise pattern, but it was really good readable and understandable. So something like that - to be well readable in mentioned contexts.

milo avatar Nov 23 '21 19:11 milo

What about calling it simply Loader or DataLoader?

MartinMystikJonas avatar Nov 23 '21 23:11 MartinMystikJonas