diff --git a/DESIGN.md b/DESIGN.md index 15381bd..c706f54 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -865,7 +865,11 @@ knows only its arity, and a bare or partially applied reference is the same adap any number of times, or call it for a fresh coroutine, as it would a zero-argument `async def` (`asyncio.run(job())`): `_pf_Async.__call__` is `async def __call__(self): return await self`. Python 3.14's `asyncio.run` accepts any awaitable, but 3.12 and 3.13 want a coroutine, so code that -must run on them calls the value. +must run on them calls the value. Cold also means an `extern` whose Python call starts work at once +(a call returning an `asyncio` Task or Future, or a JS promise under Pyodide) now starts it at the +await: two such calls bound with `let` and awaited afterwards run one after the other, where they +used to overlap, and awaiting one twice makes the call twice. A program overlaps work by handing it +to `Async.parallel` (or `race`) together, which starts every element before awaiting any. **Async combinators — the `Async` module.** `async { }` builds an `Async a`; the module is the rest of what F#'s `Async` offers that `asyncio` can supply, typed once @@ -962,7 +966,9 @@ holds (a lifetime detail a façade hides). The cookbook's `browser/` entry is th `Dom` module a program imports, with a counter page as its consumer. Two things fall out of work already done: a click handler is an ordinary one-parameter Pyfun function (Python calls it with the event, §6's calling convention), and a JS `Promise` is awaitable under Pyodide, so an extern over a -JS async API is typed `-> Async a` and awaited with `let!` like any other, no bridge to build. +JS async API is typed `-> Async a` and awaited with `let!` like any other, no bridge to build. Since +an `Async` is cold (§8), the JS call is made at the await, so requests meant to overlap go to +`Async.parallel` together. Signalling for a peer-to-peer transport is the program's problem, not the language's. **Derived codecs — `Decode.auto` and `Encode.auto`.** A program that speaks to itself over a wire diff --git a/docs/src/learn/18-async.md b/docs/src/learn/18-async.md index 4c14de4..3264c92 100644 --- a/docs/src/learn/18-async.md +++ b/docs/src/learn/18-async.md @@ -76,6 +76,11 @@ The block runs twice, once for each `let!`. A Python coroutine can only be await await runs it again from the start. F# calls this a *cold* async. It means you can keep an `Async` in a variable, pass it to a function that retries it, or put the same one in a list twice. +It also means nothing starts early. Two `let!` lines in a row run one after the other, because the +second value has not started by the time the first finishes. When you want several things to run at +the same time, such as two web requests, hand them to `Async.parallel` together, which you will meet +in a moment. + ## Running one at the top level An `Async` value has to be awaited somewhere, and at the top level there is no enclosing `async` diff --git a/docs/src/notebooks/18-async.ipynb b/docs/src/notebooks/18-async.ipynb index c15161e..e84704d 100644 --- a/docs/src/notebooks/18-async.ipynb +++ b/docs/src/notebooks/18-async.ipynb @@ -115,6 +115,11 @@ "await runs it again from the start. F# calls this a *cold* async. It means you can keep an `Async` in\n", "a variable, pass it to a function that retries it, or put the same one in a list twice.\n", "\n", + "It also means nothing starts early. Two `let!` lines in a row run one after the other, because the\n", + "second value has not started by the time the first finishes. When you want several things to run at\n", + "the same time, such as two web requests, hand them to `Async.parallel` together, which you will meet\n", + "in a moment.\n", + "\n", "## Running one at the top level\n", "\n", "An `Async` value has to be awaited somewhere, and at the top level there is no enclosing `async`\n",