Skip to content

Commit 03a01f5

Browse files
committed
Add documentation for Future
Also temporarily remove `*:` tuple comparison due to a scaladoc crash, see scala/scala3#19925
1 parent 63ae61d commit 03a01f5

File tree

4 files changed

+78
-41
lines changed

4 files changed

+78
-41
lines changed

shared/src/main/scala/async/Async.scala

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,16 @@ object Async:
121121
/** Checks whether data is available at present and pass it to `k` if so. Calls to `poll` are always synchronous and
122122
* non-blocking.
123123
*
124-
* If no element is available, returns `false` immediately. If there is (or may be) data available, `k` is locked
125-
* and if it fails, `true` is returned to signal this source's general availability. If locking `k` succeeds, only
126-
* return `true` iff `k` is completed (it is always unlocked nevertheless).
124+
* The process is as follows:
125+
* - If no data is immediately available, return `false` immediately.
126+
* - If there is data available, attempt to lock `k`.
127+
* - If `k` is no longer available, `true` is returned to signal this source's general availability.
128+
* - If locking `k` succeeds:
129+
* - If data is still available, complete `k` and return true.
130+
* - Otherwise, unlock `k` and return false.
131+
*
132+
* Note that in all cases, a return value of `false` indicates that `k` should be put into `onComplete` to receive
133+
* data in a later point in time.
127134
*
128135
* @return
129136
* Whether poll was able to pass data to `k`. Note that this is regardless of `k` being available to receive the

shared/src/main/scala/async/futures.scala

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,36 @@
11
package gears.async
22

3-
import TaskSchedule.ExponentialBackoff
4-
import AsyncOperations.sleep
5-
63
import scala.collection.mutable
7-
import mutable.ListBuffer
8-
94
import java.util.concurrent.atomic.AtomicBoolean
105
import java.util.concurrent.CancellationException
116
import scala.compiletime.uninitialized
12-
import scala.util.{Failure, Success, Try}
137
import scala.annotation.unchecked.uncheckedVariance
148
import scala.annotation.tailrec
159
import scala.util
10+
import scala.util.{Failure, Success, Try}
1611
import scala.util.control.NonFatal
1712

18-
/** A cancellable future that can suspend waiting for other asynchronous sources
13+
/** Futures are [[Async.Source Source]]s that has the following properties:
14+
* - They represent a single value: Once resolved, [[Async.await await]]-ing on a [[Future]] should always return the
15+
* same value.
16+
* - They can potentially be cancelled, via [[Cancellable.cancel the cancel method]].
17+
*
18+
* There are two kinds of futures, active and passive.
19+
* - '''Active''' futures are ones that are spawned with [[Future.apply]] and [[Task.start]]. They require the
20+
* [[Async.Spawn]] context, and run on their own (as long as the [[Async.Spawn]] scope has not ended). Active
21+
* futures represent concurrent computations within Gear's structured concurrency tree. Idiomatic Gears code should
22+
* ''never'' return active futures. Should a function be async (i.e. takes an [[Async]] context parameter), they
23+
* should return values or throw exceptions directly.
24+
* - '''Passive''' futures are ones that are created by [[Future.Promise]] (through
25+
* [[Future.Promise.asFuture asFuture]]) and [[Future.withResolver]]. They represent yet-arrived values coming from
26+
* ''outside'' of Gear's structured concurrency tree (for example, from network or the file system, or even from
27+
* another concurrency system like [[scala.concurrent.Future Scala standard library futures]]). Idiomatic Gears
28+
* libraries should return this kind of [[Future]] if deemed neccessary, but functions returning passive futures
29+
* should ''not'' take an [[Async]] context.
1930
*/
2031
trait Future[+T] extends Async.OriginalSource[Try[T]], Cancellable
2132

2233
object Future:
23-
2434
/** A future that is completed explicitly by calling its `complete` method. There are three public implementations
2535
*
2636
* - RunnableFuture: Completion is done by running a block of code
@@ -89,6 +99,11 @@ object Future:
8999
*/
90100
private class RunnableFuture[+T](body: Async.Spawn ?=> T)(using ac: Async) extends CoreFuture[T]:
91101

102+
/** RunnableFuture maintains its own inner [[CompletionGroup]], that is separated from the provided Async
103+
* instance's. When the future is cancelled, we only cancel this CompletionGroup. This effectively means any
104+
* `.await` operations within the future is cancelled *only if they link into this group*. The future body run with
105+
* this inner group by default, but it can always opt-out (e.g. with [[uninterruptible]]).
106+
*/
92107
private var innerGroup: CompletionGroup = CompletionGroup()
93108

94109
private def checkCancellation(): Unit =
@@ -150,13 +165,13 @@ object Future:
150165

151166
end RunnableFuture
152167

153-
/** Create a future that asynchronously executes [[body]] that defines its result value in a [[Try]] or returns
154-
* [[Failure]] if an exception was thrown.
168+
/** Create a future that asynchronously executes `body` that wraps its execution in a [[scala.util.Try]]. The returned
169+
* future is linked to the given [[Async.Spawn]] scope by default, i.e. it is cancelled when this scope ends.
155170
*/
156171
def apply[T](body: Async.Spawn ?=> T)(using async: Async, spawnable: Async.Spawn & async.type): Future[T] =
157172
RunnableFuture(body)
158173

159-
/** A future that immediately terminates with the given result. */
174+
/** A future that is immediately completed with the given result. */
160175
def now[T](result: Try[T]): Future[T] =
161176
val f = CoreFuture[T]()
162177
f.complete(result)
@@ -172,7 +187,6 @@ object Future:
172187
inline def rejected(exception: Throwable): Future[Nothing] = now(Failure(exception))
173188

174189
extension [T](f1: Future[T])
175-
176190
/** Parallel composition of two futures. If both futures succeed, succeed with their values in a pair. Otherwise,
177191
* fail with the failure that was returned first.
178192
*/
@@ -190,23 +204,24 @@ object Future:
190204
case Right(Failure(ex)) => r.reject(ex)
191205
})
192206

193-
/** Parallel composition of tuples of futures. Future.Success(EmptyTuple) might be treated as Nil.
194-
*/
195-
def *:[U <: Tuple](f2: Future[U]): Future[T *: U] = Future.withResolver: r =>
196-
Async
197-
.either(f1, f2)
198-
.onComplete(Listener { (v, _) =>
199-
v match
200-
case Left(Success(x1)) =>
201-
f2.onComplete(Listener { (x2, _) => r.complete(x2.map(x1 *: _)) })
202-
case Right(Success(x2)) =>
203-
f1.onComplete(Listener { (x1, _) => r.complete(x1.map(_ *: x2)) })
204-
case Left(Failure(ex)) => r.reject(ex)
205-
case Right(Failure(ex)) => r.reject(ex)
206-
})
207+
// /** Parallel composition of tuples of futures. Disabled since scaladoc is crashing with it. (https://github.com/scala/scala3/issues/19925) */
208+
// def *:[U <: Tuple](f2: Future[U]): Future[T *: U] = Future.withResolver: r =>
209+
// Async
210+
// .either(f1, f2)
211+
// .onComplete(Listener { (v, _) =>
212+
// v match
213+
// case Left(Success(x1)) =>
214+
// f2.onComplete(Listener { (x2, _) => r.complete(x2.map(x1 *: _)) })
215+
// case Right(Success(x2)) =>
216+
// f1.onComplete(Listener { (x1, _) => r.complete(x1.map(_ *: x2)) })
217+
// case Left(Failure(ex)) => r.reject(ex)
218+
// case Right(Failure(ex)) => r.reject(ex)
219+
// })
207220

208221
/** Alternative parallel composition of this task with `other` task. If either task succeeds, succeed with the
209222
* success that was returned first. Otherwise, fail with the failure that was returned last.
223+
* @see
224+
* [[orWithCancel]] for an alternative version where the slower future is cancelled.
210225
*/
211226
def or(f2: Future[T]): Future[T] = orImpl(false)(f2)
212227

@@ -229,7 +244,11 @@ object Future:
229244

230245
end extension
231246

232-
/** A promise defines a future that is be completed via the `complete` method.
247+
/** A promise is a [[Future]] that is be completed manually via the `complete` method.
248+
* @see
249+
* [[Promise$.apply]] to create a new, empty promise.
250+
* @see
251+
* [[Future.withResolver]] to create a passive [[Future]] from callback-style asynchronous calls.
233252
*/
234253
trait Promise[T] extends Future[T]:
235254
inline def asFuture: Future[T] = this
@@ -238,6 +257,7 @@ object Future:
238257
def complete(result: Try[T]): Unit
239258

240259
object Promise:
260+
/** Create a new, unresolved [[Promise]]. */
241261
def apply[T](): Promise[T] =
242262
new CoreFuture[T] with Promise[T]:
243263
override def cancel(): Unit =
@@ -389,7 +409,7 @@ class Task[+T](val body: (Async, AsyncOperations) ?=> T):
389409
if (maxRepetitions == 1) ret
390410
else {
391411
while (maxRepetitions == 0 || repetitions < maxRepetitions) {
392-
sleep(millis)
412+
AsyncOperations.sleep(millis)
393413
ret = body
394414
repetitions += 1
395415
}
@@ -408,7 +428,7 @@ class Task[+T](val body: (Async, AsyncOperations) ?=> T):
408428
else {
409429
var timeToSleep = millis
410430
while (maxRepetitions == 0 || repetitions < maxRepetitions) {
411-
sleep(timeToSleep)
431+
AsyncOperations.sleep(timeToSleep)
412432
timeToSleep *= exponentialBase
413433
ret = body
414434
repetitions += 1
@@ -427,7 +447,7 @@ class Task[+T](val body: (Async, AsyncOperations) ?=> T):
427447
repetitions += 1
428448
if (maxRepetitions == 1) ret
429449
else {
430-
sleep(millis)
450+
AsyncOperations.sleep(millis)
431451
ret = body
432452
repetitions += 1
433453
if (maxRepetitions == 2) ret
@@ -436,7 +456,7 @@ class Task[+T](val body: (Async, AsyncOperations) ?=> T):
436456
val aOld = a
437457
a = b
438458
b = aOld + b
439-
sleep(b * millis)
459+
AsyncOperations.sleep(b * millis)
440460
ret = body
441461
repetitions += 1
442462
}
@@ -451,7 +471,7 @@ class Task[+T](val body: (Async, AsyncOperations) ?=> T):
451471
@tailrec
452472
def helper(repetitions: Long = 0): T =
453473
if (repetitions > 0 && millis > 0)
454-
sleep(millis)
474+
AsyncOperations.sleep(millis)
455475
val ret: T = body
456476
ret match {
457477
case Failure(_) => ret
@@ -467,7 +487,7 @@ class Task[+T](val body: (Async, AsyncOperations) ?=> T):
467487
@tailrec
468488
def helper(repetitions: Long = 0): T =
469489
if (repetitions > 0 && millis > 0)
470-
sleep(millis)
490+
AsyncOperations.sleep(millis)
471491
val ret: T = body
472492
ret match {
473493
case Success(_) => ret
@@ -480,6 +500,11 @@ class Task[+T](val body: (Async, AsyncOperations) ?=> T):
480500

481501
end Task
482502

503+
/** Runs the `body` inside in an [[Async]] context that does *not* propagate cancellation until the end.
504+
*
505+
* In other words, `body` is never notified of the cancellation of the `ac` context; but `uninterruptible` would still
506+
* throw a [[CancellationException]] ''after `body` finishes running'' if `ac` was cancelled.
507+
*/
483508
def uninterruptible[T](body: Async ?=> T)(using ac: Async): T =
484509
val tracker = Cancellable.Tracking().link()
485510

@@ -492,7 +517,12 @@ def uninterruptible[T](body: Async ?=> T)(using ac: Async): T =
492517
if tracker.isCancelled then throw new CancellationException()
493518
r
494519

495-
def cancellationScope[T](cancel: Cancellable)(fn: => T)(using a: Async): T =
496-
cancel.link()
520+
/** Link `cancellable` to the completion group of the current [[Async]] context during `fn`.
521+
*
522+
* If the [[Async]] context is cancelled during the execution of `fn`, `cancellable` will also be immediately
523+
* cancelled.
524+
*/
525+
def cancellationScope[T](cancellable: Cancellable)(fn: => T)(using a: Async): T =
526+
cancellable.link()
497527
try fn
498-
finally cancel.unlink()
528+
finally cancellable.unlink()

shared/src/test/scala/ChannelBehavior.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import gears.async.{
1111
}
1212
import gears.async.default.given
1313
import gears.async.AsyncOperations.*
14-
import Future.{*:, zip}
14+
import Future.zip
1515

1616
import java.util.concurrent.CancellationException
1717
import scala.collection.mutable

shared/src/test/scala/TaskScheduleBehavior.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import gears.async.{Async, Future, Task, TaskSchedule}
22
import gears.async.default.given
3-
import Future.{*:, zip}
3+
import Future.zip
44

55
import scala.concurrent.ExecutionContext
66
import scala.util.{Failure, Success, Try}

0 commit comments

Comments
 (0)