Future: обещание результата

Что такое Future

Async\Future — это объект, представляющий результат операции, который может быть ещё не готов. Future позволяет:

Future похож на Promise в JavaScript, но интегрирован с корутинами TrueAsync.

Future и FutureState

Future разделён на два класса с чётким разделением ответственности:

<?php
use Async\Future;
use Async\FutureState;

// Создаём FutureState — он владеет состоянием
$state = new FutureState();

// Создаём Future — он даёт доступ к результату
$future = new Future($state);

// Передаём $future потребителю
// Передаём $state продюсеру

// Продюсер завершает операцию
$state->complete(42);

// Потребитель получает результат
$result = $future->await(); // 42
?>

Такое разделение гарантирует, что потребитель не может случайно завершить Future — только держатель FutureState имеет на это право.

Создание Future

Через FutureState

<?php
use Async\Future;
use Async\FutureState;
use function Async\spawn;

$state = new FutureState();
$future = new Future($state);

// Завершаем в другой корутине
spawn(function() use ($state) {
    $data = file_get_contents('https://api.example.com/data');
    $state->complete(json_decode($data, true));
});

$result = $future->await();
?>

Статические фабрики

Для создания уже завершённых Future:

<?php
use Async\Future;

// Успешно завершённый Future
$future = Future::completed(42);
$result = $future->await(); // 42

// Future с ошибкой
$future = Future::failed(new \RuntimeException('Что-то пошло не так'));
$result = $future->await(); // выбросит RuntimeException
?>

Цепочки трансформаций

Future поддерживает три метода трансформации, работающих аналогично Promise в JavaScript:

map() — трансформация результата

Вызывается только при успешном завершении. Возвращает новый Future с трансформированным результатом:

<?php
use Async\Future;
use Async\FutureState;

$state = new FutureState();
$future = new Future($state);

$doubled = $future->map(fn($value) => $value * 2);
$asString = $doubled->map(fn($value) => "Результат: $value");

$state->complete(21);

echo $asString->await(); // "Результат: 42"
?>

catch() — обработка ошибок

Вызывается только при ошибке. Позволяет восстановиться после исключения:

<?php
use Async\Future;
use Async\FutureState;

$state = new FutureState();
$future = new Future($state);

$safe = $future->catch(function(\Throwable $e) {
    return 'Значение по умолчанию';
});

$state->error(new \RuntimeException('Ошибка'));

echo $safe->await(); // "Значение по умолчанию"
?>

finally() — выполнение при любом исходе

Вызывается всегда — и при успехе, и при ошибке. Результат родительского Future передаётся дочернему без изменений:

<?php
use Async\Future;
use Async\FutureState;

$state = new FutureState();
$future = new Future($state);

$withCleanup = $future->finally(function($resultOrException) {
    // Освобождаем ресурсы
    echo "Операция завершена\n";
});

$state->complete('данные');

echo $withCleanup->await(); // "данные" (результат передаётся без изменений)
?>

Составные цепочки

<?php
use Async\Future;
use Async\FutureState;

$state = new FutureState();
$future = new Future($state);

$result = $future
    ->map(fn($data) => json_decode($data, true))
    ->map(fn($parsed) => $parsed['name'] ?? 'Неизвестно')
    ->catch(fn(\Throwable $e) => 'Ошибка: ' . $e->getMessage())
    ->finally(function($value) {
        // Логирование
    });

$state->complete('{"name": "PHP"}');
echo $result->await(); // "PHP"
?>

Независимые подписчики

Каждый вызов map() на одном и том же Future создаёт независимую цепочку. Подписчики не влияют друг на друга:

<?php
use Async\Future;
use Async\FutureState;
use function Async\await;

$state = new FutureState();
$future = new Future($state);

// Две независимые цепочки от одного Future
$doubled = $future->map(fn($x) => $x * 2);
$tripled = $future->map(fn($x) => $x * 3);

$state->complete(10);

echo await($doubled) . "\n"; // 20
echo await($tripled) . "\n"; // 30
?>

Распространение ошибок в цепочке

Если исходный Future завершается с ошибкой, map() пропускается, а ошибка передаётся напрямую в catch():

<?php
use Async\Future;
use Async\FutureState;
use function Async\await;

$state = new FutureState();
$future = new Future($state);

$result = $future
    ->map(function($value) {
        echo "Этот код не выполнится\n";
        return $value;
    })
    ->catch(function(\Throwable $e) {
        return 'Восстановлено: ' . $e->getMessage();
    });

$state->error(new \RuntimeException('Ошибка источника'));

echo await($result) . "\n"; // "Восстановлено: Ошибка источника"
?>

Если исключение возникает внутри map(), оно перехватывается последующим catch():

<?php
use Async\Future;
use Async\FutureState;
use function Async\await;

$state = new FutureState();
$future = new Future($state);

$result = $future
    ->map(function($x) {
        throw new \RuntimeException('Ошибка в map');
    })
    ->catch(function(\Throwable $e) {
        return 'Перехвачено: ' . $e->getMessage();
    });

$state->complete(42);

echo await($result) . "\n"; // "Перехвачено: Ошибка в map"
?>

Ожидание результата

Через функцию await()

<?php
use function Async\await;

$result = await($future);

Через метод $future->await()

<?php
$result = $future->await();

// С таймаутом отмены
$result = $future->await(Async\timeout(5000));

Отмена Future

<?php
use Async\CancellationError;

// Отмена с сообщением по умолчанию
$future->cancel();

// Отмена с кастомной ошибкой
$future->cancel(new CancellationError('Операция больше не нужна'));

Подавление предупреждений: ignore()

Если Future не используется (не вызван await(), map(), catch() или finally()), TrueAsync выдаст предупреждение. Чтобы явно подавить это предупреждение:

<?php
$future->ignore();

Также, если Future завершился с ошибкой и эта ошибка не была обработана, TrueAsync предупредит об этом. ignore() подавляет и это предупреждение.

FutureState: завершение операции

complete() — успешное завершение

<?php
$state->complete($result);

error() — завершение с ошибкой

<?php
$state->error(new \RuntimeException('Ошибка'));

Ограничения

<?php
$state->complete(1);
$state->complete(2); // AsyncException: FutureState is already completed

Диагностика

Оба класса (Future и FutureState) предоставляют методы диагностики:

<?php
// Проверка состояния
$future->isCompleted(); // bool
$future->isCancelled(); // bool

// Где был создан Future
$future->getCreatedFileAndLine();  // [string $file, int $line]
$future->getCreatedLocation();     // "file.php:42"

// Где был завершён Future
$future->getCompletedFileAndLine(); // [string|null $file, int $line]
$future->getCompletedLocation();    // "file.php:55" или "unknown"

// Информация об ожидании
$future->getAwaitingInfo(); // array

Практический пример: HTTP-клиент

<?php
use Async\Future;
use Async\FutureState;
use function Async\spawn;

function httpGet(string $url): Future {
    $state = new FutureState();
    $future = new Future($state);

    spawn(function() use ($state, $url) {
        try {
            $response = file_get_contents($url);
            $state->complete($response);
        } catch (\Throwable $e) {
            $state->error($e);
        }
    });

    return $future;
}

// Использование
$userFuture = httpGet('https://api.example.com/user/1')
    ->map(fn($json) => json_decode($json, true))
    ->catch(fn($e) => ['error' => $e->getMessage()]);

$result = $userFuture->await();
?>

См. также