EsFuture
Description
An object representing the result of an asynchronous computation.
An asynchronous computation cannot provide a result immediately when it is started, unlike a synchronous computation which does compute a result immediately by either returning a value or by raising an exception. An asynchronous computation may need to wait for something external to the program (reading a file, querying a database, fetching a web page) which takes time.
Instead of blocking all computations until the result is available, the asynchronous computation immediately answers an EsFuture which will eventually "complete" with the result.
Asynchronous programming
With an EsFuture, you can manually register handler blocks that handle the value, or error, once it is available. For example:
future := self getFuture.
future
then: [:value | self handleValue: value]
catch: [:error | self handleError: error]
Since an EsFuture can be completed in two ways, either with a value (if the asynchronous computation succeeded) or with an error (if the computation failed), you can install handlers for either or both cases.
In some cases we say that a future is completed with another future. This is a short way of stating that the future is completed in the same way, with the same value or error, as the other future once that other future itself completes.
Most functions in the platform libraries that complete a future (for example EsPromise>>complete or EsFuture class>>value:), also accepts another future, and automatically handles forwarding the result to the future being completed.
The result of registering handler blocks is itself an EsFuture, which in turn is completed with the result of invoking the corresponding handler with the original future's result. The new future is completed with an error if the invoked handler block raises and exception.
For example:
promise := EsPromise new.
future := promise future.
successor := future
then: [:value |
"Evaluated when the future is completed with a value.
The successor is completed with the value 42"
42]
catch: [:error |
"The successor is completed with 499 or an Exception"
self canHandle ifTrue: [499] ifFalse: [Exception signal: e asString]].
If a future does not have any registered handler when it completes with an error, it forwards the error message to the error-handler for uncaught errors. (@See EsAsyncUncaughtErrorHandler>>handleError:). This behavior ensures that no error is silently dropped. However, it also means that error handlers should be installed early, so that they are present as soon as a future is completed with an error.
The following example demonstrates this potential bug:
promise := EsPromise new.
[| future |
future := promise future.
(Delay forMilliseconds: 100) wait.
"The error-handler is not attached until 100 ms after the future has
been received. If the future fails before that, the error is
forwarded to the global error-handler, even though there is code
(just below) to eventually handle the error."
future
then: [:value | self useValue: value]
catch: [:error | self handleError: error]] fork.
promise completeError: 'error'
Synchronous vs Asynchronous Example:
"Synchronous code."
[ | value |
value := self value.
self assert: [ value isNil]
] on: Exception do: [:ex |
ex exitWith: 499
]
Equivalent asynchronous code, based on futures:
future := EsFuture on: [self foo].
(future then: [:value | self bar: value]) catch: [:e | 499]
Similar to the synchronous code, the error handler (registered with #catch: is handling any errors thrown by either #foo or #bar. If the error-handler had been registered as the onError parameter of a #then:catch: call, it would not catch errors from the #bar call.
Futures can have more than one handler-pair registered. Each successor is treated independently and is handled as if it was the only successor.
A future may also fail to ever complete. In that case, no handler blocks are called. That situation should generally be avoided if possible, unless it's very cleary documented.
Class Methods
all:
Waits for multiple futures to complete and collects their results.
Answers a future which will complete once all the provided futures
have completed, either with their results, or with an error if any
of the provided futures fail.
The value of the answered future will be a list of all the values that
were produced in the order that the futures are provided by iterating
over @futures
If any future completes with an error,
then the answered future completes with that error.
If further futures also complete with errors, those errors are discarded.
All futures must complete before the answered future is completed (still with
the first error; the remaining errors are silently dropped)
Arguments:
futures - <SequenceableCollection>
Answers:
<EsFuture>
all:eagerError:onCleanUp:
Waits for multiple futures to complete and collects their results.
Answers a future which will complete once all the provided futures
have completed, either with their results, or with an error if any
of the provided futures fail.
The value of the answered future will be a list of all the values that
were produced in the order that the futures are provided by iterating
over @futures
If any future completes with an error,
then the answered future completes with that error.
If further futures also complete with errors, those errors are discarded.
If @eagerError is true, the answered future completes with an error
immediately on the first error from one of the futures. Otherwise all
futures must complete before the answered future is completed (still with
the first error; the remaining errors are silently dropped).
In the case of an error, @onCleanUp (if provided), is invoked on any
non-null result of successful futures.
This makes it possible to cleanUp resources that would otherwise be
lost (since the answered future does not provide access to these values).
@onCleanUp is not evaluated if there is no error.
Arguments:
futures - <SequenceableCollection>
eagerError - <Boolean>
onCleanUp - <Block>
Answers:
<EsFuture>
all:onCleanUp:
Waits for multiple futures to complete and collects their results.
Answers a future which will complete once all the provided futures
have completed, either with their results, or with an error if any
of the provided futures fail.
The value of the answered future will be a list of all the values that
were produced in the order that the futures are provided by iterating
over @futures
If any future completes with an error,
then the answered future completes with that error.
If further futures also complete with errors, those errors are discarded.
In the case of an error, @onCleanUp (if provided), is invoked on any
non-null result of successful futures.
This makes it possible to cleanUp resources that would otherwise be
lost (since the answered future does not provide access to these values).
@onCleanUp is not evaluated if there is no error.
Arguments:
futures - <SequenceableCollection>
onCleanUp - <Block> 0-arg
Answers:
<EsFuture>
any:
Answers the result of the first future in @futures to complete with
a value or an error.
If @futures is empty, or none of the futures complete, then the answered
future never completes
Arguments:
futures - <SequenceableCollection>
Answers:
<EsFuture>
error:
Answers a future that completes with an error @anError
and default stack trace
The created future will be completed with an error in a future
smalltalk process leaving enough time for error handlers to be
registered.
If an error handler isn't registered before the future completes, the
error will be considered unhandled
@Note: Use EsPromise to create a future and complete it later
Examples:
self assert: [(EsFuture error: 'Issue') waitFor = 'Issue']
Arguments:
anError - <Object>
Answers:
<EsFuture>
error:stackTrace:
Answers a future that completes with an error @anError
and optional stack trace.
The created future will be completed with an error in a future
smalltalk process leaving enough time for error handlers to be
registered.
If an error handler isn't registered before the future completes, the
error will be considered unhandled
@Note: Use EsPromise to create a future and complete it later
Arguments:
anError - <Object>
aStackTrace - <EsAsyncStackTrace>
Answers:
<EsFuture>
forEach:do:
Evaluates @aBlock for each element in @elements.
Evaluates @aBlock with each element in @elements in order.
If the evaluation of @aBlock answers an EsFuture, the iteration waits
until the future is completed before continuing with the next element.
Answers an EsFuture that completes with nil when all elements have been
processed.
Non-EsFuture answered values, and completion-values of answered EsFutures,
are discarded.
Any error from @aBlock will stop the iteration and be reported in the answered
EsFuture
Arguments:
elements - <SequenceableCollection>
aBlock - <Block> 1-arg
Answers:
<EsFuture>
new
Answer a new instance of this future
Answers:
<EsFuture>
on:
Creates a future containing the result of calling @aComputation
asynchronously with EsAsyncTaskScheduler>>scheduleTask:.
If the result of executing @aComputation raises an exception,
the answered future is completed with the error.
If the answered value is itself an EsFuture, completion of
the created future will wait until the answered future completes,
and will then complete with the same result.
If a non-future value is answered, the answered future is completed
with that value.
If @aComputation is a non-evaluatable object (i.e. not a Block or Message),
then its #value is taken, which in most cases is just the object itself.
The future will complete with the object>>value
Arguments:
aComputation - <Block> - The value of the Block is evaluated
<DirectedMessage> The receiver>>selector is sent
<Object> Object>>value is sent
Answers:
<EsFuture>
on:delayed:
Creates a future that runs its computation after a delay.
@aComputation will be executed after the given @aDurationOrMs has passed,
and the future is completed with the result of the computation.
If @aComputation answers a future,
the future answered by this #on:delayed: method will complete with the value or
error of that future.
If the duration is 0 or less, it completes no sooner than when a scheduled
task submitted at the lowest priority is able to exectute a separate timer
process at the highest priority. This is to allow for all scheduled tasks to
run before the high-priority timer comples the task.
If @aComputation is a non-evaluatable object (i.e. not a Block or Message),
then its #value is taken, which in most cases is just the object itself.
The future will eventually complete with the object>>value
If evaluating @aComputation raises an error, the created future will complete
with that error.
@see also EsPromise for a way to create and complete a future at a later time
that isn't necessarily after a known fixed duration
Examples:
EsFuture on: [Transcript show: 'One second has passed'] delayed: (Duration seconds: 1)
Arguments:
aComputation - <Block> - The value of the Block is evaluated
<DirectedMessage> The receiver>>selector is sent
<Object> Object>>value is sent
aDurationOrMs - <Duration>duration object
<Integer> duration in milliseconds
Answers:
<EsFuture>
sync:
Answer a future containing the result of immediately calling
@aComputation
If calling @aComputation raises an error, the answered future is completed with the
error.
If calling @aComputation answers a future, that future is answered.
If calling @aComputation answers a non-future value, a future is answered
which has beem completed with that value.
i.e. self assert: [(EsFuture sync: 12) waitFor = 12]
If @aComputation is a non-evaluatable object (i.e. not a Block or Message),
then its #value is taken, which in most cases is just the object itself.
The future will complete with the object>>value
Arguments:
aComputation - <Block> - The value of the Block is evaluated
<DirectedMessage> The receiver>>selector is sent
<Object> Object>>value is sent
Answers:
<EsFuture>
value:
Answers a future completed with @aValue.
If @aValue is a future, the created future waits for @aValue
future to complete, then completes with the same result.
If @aValue is not a future, the created future is completed
with @aValue value.
Use <EsPromise> to create a future and complete it later
Examples:
self assert: [(EsFuture value: 2001) waitFor = 2001]
Arguments:
aValue - <Object> value object or future
Answers:
<EsFuture>
Instance Methods
catch:
Handles errors emitted by this future unconditionally.
This is the asynchronous equivalent of an 'on:do:' expression.
@see EsFuture>>catch:if: for more details.
Arguments:
onError - <Block> 0, 1 to 2-arg with error and stackTrace
Answers:
<EsFuture>
catch:if:
Handles errors emitted by this future.
This is the asynchronous equivalent of an 'on:do:' expression.
Answers a new future that will be completed with either the result of
this future or the result of evaluating @onError action.
If this future completes with a value,
the answered future completes with the same value.
If this future completes with an error,
then @shouldCatchError is first evaluated with the error value.
If @shouldCatchError answers false, the exception is not handled by this '#catch:if'
and the answered future completes with the same error and stack trace as this future.
If @shouldCatchError answers true, @onError is evaluated with the error and possibly
stack trace, and the answered future is completed with the result of this call in exactly
the same way as for '#then:catch:` onError evaluation.
If @shouldCatchError is nil, it defaults to a block that always answers true.
The evaluation of @shouldCatchError should not throw, but if it does, it is handled as
if the evaluation of @onError had thrown.
Note that futures don't delay reporting of errors until listeners are
added. If the first `#catch:` (or `#then:`) call happens after this future
has completed with an error then the error is reported as unhandled error.
Arguments:
onError - <Block> 0, 1 or 2-arg with error and stackTrace
shouldCatchError - <Block> test block to determine if catch is applicable
<UndefinedObject> implicit test block [true]
Answers:
<EsFuture>
ensure:
Registers an @onCompletion action to be called when this future completes.
The @onCompletion action is evaluated when this future completes, whether it
does so with a value or an error.
This is the asynchronous equivalent of an 'ensure' block.
The future answered by this call, 'f', will complete the same way as this future
unless an error occurs in the @onCompletion action, or in a future answered by
@onCompletion. If the evaluation of @onCompletion does not answer a future,
its answered value is ignored
If the evaluation of @onCompletion raises an error, the 'f' is completed with the
error.
If the evaluation of @onCompletion answers a future, 'f2', then completion of 'f'
is delayed until 'f2' completes. If 'f2' completes with an error, that will be the result
of 'f' too. The value of 'f2' is always ignored.
This method is equivalent to:
EsFuture>>ensure: onCompletion
^self
then: [:v | | f2 |
f2 := onCompletion value.
f2 isEsFutureCompatible ifTrue: [f2 then: [v] ifFalse: [v]]
catch: [:e | | f2 |
f2 := onCompletion value.
f2 isEsFutureCompatible ifTrue: [f2 then: [Exception signal: e]] ifFalse: [Exception signal: e]].
Arguments:
onCompletion - <Block> 0-arg
Answers:
<EsFuture>
hasEsFutureInterface
Answer true if this object has an EsFuture interface, false otherwise.
Answers:
<Boolean>
then:
Register a handler to be evaluated when this future completes with a value.
When this future completes with a value,
the @onValue handler will be evaluated with that value.
If this future is already completed, the handler will not be evaluated
immediately, but will be scheduled to run in a later task.
The @onValue handler can accept either zero or one argument
where the argument is the value object.
Answers a new future which is completed with the result of the evaluation of @onValue
(if this future completes with a value)
If this future completes with an error,
the error is forwarded directly to the answered future.
@see EsFuture>>then:catch: for more complete details
Arguments:
onValue - <Block> 0 or 1-arg injected with completed value (if any)
Answers:
<EsFuture>
then:catch:
Register handlers to be evaluated when this future completes.
When this future completes with a value,
the @onValue handler will be evaluated with that value.
If this future is already completed, the handler will not be evaluated
immediately, but will be scheduled to run in a later task.
The @onValue handler can accept either zero or one argument
where the argument is the value object.
If @onError is provided, and this future completes with an error,
the @onError handler is evaluated with that error and its stack trace.
The @onError handler can accept either zero, one or two arguments
where the arguments are the error object and stack trace.
The @onError handler must answer a value or future that can be used
to complete the answered future.
Answers a new future which is completed with the result of the evaluation of @onValue
(if this future completes with a value) or to @onError (if this future completes with an error).
If the invoked handler raises an error,
the answered future is completed with the raised error and stack trace for the error.
In the case of @onError,
if the exception raised is identical to the error argument to @onError,
the raised error is considered a rethrow and the original stack trace is used
instead.
If the handler answers a future
the future answered by #then: will be completed with
the same result as the future answered by the handler.
If @onError is not given, and this future completes with an error,
the error is forwarded directly to the answered future.
In most cases, it is more readable to use #catch: separately, possibly
with an #if: parameter (i.e. catch:if:), instead of handling both value
and error in a single #then: call.
Note that futures don't delay reporting of errors until listeners are
added. If the first #then: or #catch: call happens after this future
has completed with an error then the error is reported as unhandled error.
See the description in the class comments.
Arguments:
onValue - <Block> 0 or 1-arg injected with completed value (if any)
onError - <Block> 0, 1 or 2-arg injected with completed error and stack trace (if any)
Answers:
<EsFuture>
timeout:then:
Timeout the future computation after @aDurationOrMs has passed.
Answers a new future that completes with the same value as this future,
if this future completes in time.
If this future does not complete before @aDurationOrMs has passed,
the @onTimeout block is evaluated instead, and its result (whether it
returns or raises) is used as the result of the answered future.
The @onTimeout block can answer a value or a future.
If @onTimeout is nil, a timeout will cause the answered future to complete
with a timeout exception
Arguments:
aDurationOrMs - <Duration> duration object
<Integer> duration in milliseconds
onTimeout - <Block> 0-arg block
Answers:
<EsFuture>
waitFor
BLOCKING: Wait for the future computation to complete.
Answers the result of the future.
@Note: Usage of Blocking APIs are typically discouraged in
asynchronous programming. However, in certain situations
it can be necessary to block, so it is provided.
Answers:
<Object>
waitFor:
BLOCKING: Wait for at most @aDurationOrMs for the future computation
to complete. Answers true if the future completes in time,
otherwise answers false.
@Note: Usage of Blocking APIs are typically discouraged in
asynchronous programming. However, in certain situations
it can be necessary to block, so it is provided.
Arguments:
aDurationOrMs - <Duration> duration object
<Integer> duration in milliseconds
Answers:
<Boolean>