From 3cf0716dcddedadf06ea2942fc68f4e38d044269 Mon Sep 17 00:00:00 2001 From: simontreanor Date: Sun, 18 Oct 2026 00:07:34 +0100 Subject: [PATCH] docs: a cold Async starts at the await, so overlap goes through Async.parallel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since #177 an extern whose Python call starts work at once (an asyncio Task, a JS promise) starts it at the await instead. Two such calls bound with let and awaited afterwards used to overlap and now run one after the other, and awaiting one twice makes the call twice. Checked against the 0.9.0 wheel: the same program started both at 0.0s there and the second at 0.5s now, while Async.parallel overlapped under both. DESIGN §8 and the Pyodide passage say so, and lesson 18 tells the reader to use Async.parallel for overlap. --- DESIGN.md | 10 ++++++++-- docs/src/learn/18-async.md | 5 +++++ docs/src/notebooks/18-async.ipynb | 5 +++++ 3 files changed, 18 insertions(+), 2 deletions(-) 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",