Skip to content
CryoCryo home
Stdlibfuture

combinator

import std::future::combinator; · source

Join, TryJoin, Select, and Timeout are ordinary futures that own the futures they compose and drive them from their own poll. Every child is polled with the same Context, so they all register the composing task's waker: whichever child becomes ready wakes the task, the task re-polls the combinator, and the combinator re-polls whichever children have not finished.

One task drives the whole group. No child is spawned, and none needs an executor.

Futures

type struct Futures {
    static join<A, B, OA, OB>(a: A, b: B) -> Join<A, B, OA, OB>
    where A: Future<OA>, B: Future<OB>;
    static try_join<A, B, T1, T2, E>(a: A, b: B) -> TryJoin<A, B, T1, T2, E>
    where A: Future<Result<T1, E>>, B: Future<Result<T2, E>>;
    static select<A, B, OA, OB>(a: A, b: B) -> Select<A, B, OA, OB>
    where A: Future<OA>, B: Future<OB>;
    static timeout<F, O>(dur: Duration, fut: F) -> Timeout<F, O>
    where F: Future<O>;
    static timeout_at<F, O>(deadline: Instant, fut: F) -> Timeout<F, O>
    where F: Future<O>;
}

Build them through Futures, which is a namespace rather than a value — a combinator is generic over its children's types as well as their outputs, and a child produced by an async function has a compiler-generated type no caller can name. Constructing through statics whose bounds infer the whole set is what makes them constructible at all.

ConstructorCompletes with
Futures::join(a, b)(OA, OB) once both have finished.
Futures::try_join(a, b)Ok((T1, T2)) once both succeed, or the first Err either reports — dropping, and so cancelling, the sibling.
Futures::select(a, b)Selected<OA, OB> as soon as either finishes.
Futures::timeout(dur, f)Result<O, Elapsed>.
Futures::timeout_at(deadline, f)The same, against an absolute Instant.

Join and TryJoin

type struct Join<A, B, OA, OB> {
    a:      A;
    b:      B;
    a_out:  Option<OA>;
    b_out:  Option<OB>;
    a_done: boolean;
    b_done: boolean;
}
type struct TryJoin<A, B, T1, T2, E> {
    a:      A;
    b:      B;
    a_out:  Option<T1>;
    b_out:  Option<T2>;
    a_done: boolean;
    b_done: boolean;
    done:   boolean;
}

Join completes when both children have; TryJoin completes early with the first Err either child reports, dropping the sibling — which cancels it.

Select

type struct Select<A, B, OA, OB> {
    a:    A;
    b:    B;
    done: boolean;
}
type enum Selected<L, R> {
    First(L);
    Second(R);
}

select polls a first on every poll. That bias is deliberate and documented rather than randomized: a select returns on the first ready child, so a consistently-ready a cannot starve b within one select — it only makes the choice predictable when both are ready at once, which is exactly what a caller pairing an operation against a timeout wants.

Timeout

type struct Timeout<F, O> {
    fut:   F;
    sleep: Sleep;
    done:  boolean;
}
type struct Elapsed {
    deadline: i64;

    static at(deadline: i64) -> Elapsed;
    deadline_nanos(&this) -> i64;
}

timeout polls the operation before the deadline, so a future that becomes ready on the very poll where the timer also fires reports its value rather than a timeout. It did complete; calling that a timeout would discard a result the caller can never get back — and for an I/O future, one whose side effect has already happened.

Elapsed carries the deadline it missed, as monotonic nanoseconds on the same scale Instant::as_nanos reports.

Cancellation is a drop

A Select that completes still owns the child that lost, and a Timeout that elapses still owns the operation it was timing. Both are released when the combinator itself is dropped, and that is what cancels them: dropping a parked future runs its Drop, which is where a Sleep disarms its timer and an I/O future releases its reactor registration. TryJoin short-circuiting on an error is the same mechanism — the sibling is dropped, and the drop cancels it.

Cancellation therefore needs no separate mechanism and cannot be forgotten — it is the same drop that reclaims the memory.

Arity

These compose two futures. Higher arities nest: Futures::join(a, Futures::join(b, c)). Cryo has no variadic generics, so a fixed-arity pair plus nesting is the whole story; the alternative would be a hand-written Join3, Join4, ... tower that says nothing new.

Polling any of these after it completes panics, matching Ready. A completed future has already moved its output out, so a second poll has nothing to return — and would re-poll children that already finished, itself a contract violation.

Trait implementations

implement<A, B, OA, OB> trait Future<(OA, OB)> for struct Join<A, B, OA, OB>
where A: Future<OA>, B: Future<OB>

implement<A, B, T1, T2, E> trait Future<Result<(T1, T2), E>> for struct TryJoin<A, B, T1, T2, E>
where A: Future<Result<T1, E>>, B: Future<Result<T2, E>>

implement<A, B, OA, OB> trait Future<Selected<OA, OB>> for struct Select<A, B, OA, OB>
where A: Future<OA>, B: Future<OB>

implement<F, O> trait Future<Result<O, Elapsed>> for struct Timeout<F, O>
where F: Future<O>