84 Responsive Applications without async/await

Predica applications use ordinary synchronous evaluations even when database access requires I/O or a derived value requires expensive computation. A function, query, derived attribute, or display evaluation runs from entry to completion. It does not suspend and is not resumed later.

This does not make the application block on unavailable information. Accessing a nonresident database object expresses demand for that object and can initiate asynchronous I/O. Accessing an unavailable expensive result likewise expresses demand for its calculation. The access reports the information currently available, including pending or partial state, and the current evaluation deals with that state and continues.

synchronous evaluation
    + demand-triggering access
    + asynchronously published information
    + later independent evaluations
    = responsive application

Predica therefore does not make an asynchronous procedure merely look synchronous. Application evaluations are genuinely synchronous. It is the availability of the information they observe that changes asynchronously.

Partial availability is ordinary information

An application must distinguish the logical value of an expression from the physical availability of the information needed to evaluate it. A database object may exist in the reader's snapshot without its representation currently being resident in memory. A derived value may be demanded while its inputs or calculation are still pending.

Depending on the kind of value and the needs of its reader, an evaluation may observe states such as:

  • available: the value can be used now;
  • pending: work needed to obtain the value has been requested but has not completed;
  • partial: a useful result is available but further work can refine or extend it;
  • refreshing: an earlier result remains usable while a replacement is calculated;
  • failed: the requested loading or calculation failed; and
  • absent: the logical value is not present in the snapshot.

Pending is not an incomplete execution. It is a state from which ordinary expressions can calculate another value. A display can derive a placeholder from pending image data, a progress indicator from partial work, or the text Calculating… from a demanded result that is not yet available.

Logical absence and physical nonresidency are particularly important to keep separate. An optional value with cardinality zero is absent from the database state. An object with cardinality one remains logically present when its bytes are nonresident. The latter access may initiate loading; it must not be misreported as logical absence. Object-qualified values and this distinction are described further in Objects.

Database access expresses demand

From application code, navigation to a database object is an ordinary synchronous operation. Physically, every such access is potentially the start of asynchronous I/O. If the required rendition is resident for the reader's snapshot, the access can use it immediately. If it is nonresident, the access records demand, arranges or joins a request to load it, and reports pending availability without blocking the reader.

Demand belongs to the information, rather than to a suspended invocation. Several views that access the same object can share one resident rendition and one outstanding load. If a view ceases to require the object, no continuation owned by that view remains to be resumed. The runtime can reprioritise, coalesce, or abandon work according to current aggregate demand.

The same principle applies to expensive Derived Attributes. Reading a derived attribute demands its result. If all required inputs are available, the Dependency Graph System (DGS) can schedule its calculation. If some inputs are pending, the calculation is not started until it is ready. No procedure is started early merely so that it can suspend at a missing input.

required inputs incomplete  → calculation is not runnable
last required input arrives → calculation becomes runnable
calculation runs            → result is published

Work that can produce a useful partial result need not wait for every possible input. It runs with the availability states it accepts. Work that requires complete inputs starts only when those inputs are available. Neither case requires a continuation.

Example: progressively rendering a multiresolution image

Consider a multiresolution image whose levels contain tiled JPEG objects. A level may include an array such as:


MortonArray<object Jpeg?, numTiles.x, numTiles.y> tiles;

A display pass must finish in its frame time. It cannot wait for a desired tile to be read or decoded. When it accesses the tile, the access may initiate or reinforce demand for the necessary I/O and decoding. If the decoded tile is available, the pass renders it. If it is pending, the pass can render an available tile from a parent level, retain an earlier rendition, or draw a placeholder. It then completes the frame.

desired child tile available → render child tile
desired child tile pending   → render best available parent tile
desired child tile failed    → render failure indication or another fallback

When the child tile later becomes resident, the original display pass is not resumed. A later pass makes its own decision. Its viewport, desired resolution, frame budget, snapshot, and available parent tiles may all differ. It may use the newly resident child, choose a different parent, or find that the tile is no longer visible.

The old pass expressed demand but left no suspended rendering intention behind. The durable state is the demand, loading status, resident rendition, and relevant versions—not the old pass's instruction position or local variables.

One evaluation, one MVCC snapshot

Each reader evaluation uses one coherent MVCC snapshot. All database values used by that evaluation belong to the same visible committed state. Concurrent writers can publish newer states without changing the world observed by the reader while it is calculating. The implementation requirements are described in Snapshot Isolation.

An evaluation that encounters pending information completes using that pending state. A later evaluation takes a later snapshot and recalculates using the information and application context then current:

displayₙ   = derive(snapshotₙ,   contextₙ)
displayₙ₊₁ = derive(snapshotₙ₊₁, contextₙ₊₁)

The second evaluation is not a continuation of the first. This is important for correctness as well as responsiveness. Retaining calculated local variables from snapshotₙ and then combining them with database values from snapshotₙ₊₁ could produce a result that corresponds to no consistent database state. Alternatively, retaining snapshotₙ across indefinite I/O would keep an old state and an old application intention alive after they had ceased to be relevant.

The display is a current projection

A user interface continuously derives a presentation from current information. It does not wait for that information to become complete. The same model supports:

  • refining an image as higher-resolution tiles become resident;
  • extending a list as additional database ranges are loaded;
  • showing partial query or analysis results;
  • updating a progress bar from published progress state;
  • showing Calculating… while a demanded result is pending;
  • retaining a previous result while a changed input causes it to be refreshed; and
  • displaying a failure without preventing unrelated information from being shown.

These are derived display states, not commands surrounding an awaited call. For example, Calculating… is visible whenever the calculation is demanded and a final result is not currently available. Completion publishes a changed state; the next display evaluation derives that the message is no longer required.

The UI may evaluate on a regular frame clock or be awakened by a coarse signal that published information may have changed. Such a signal only causes a new evaluation. It does not carry a result into the UI or identify a suspended operation to resume.

Dependencies without writer-to-reader notification

The name Dependency Graph System describes the dependency semantics; it does not require the runtime to materialise a global bidirectional graph. In particular, writers need not maintain lists of readers or propagate dirty flags through an observer graph.

A writer can publish its changed object and increment a change count. A reader that caches a derived result records references to the inputs it used and the change counts it observed. When that result is demanded again, the reader compares the recorded counts with the current inputs. A mismatch makes the reader reconsider its result. Writers remain independent of readers, and readers perform validation when there is actual demand for their conclusions.

This pull-based arrangement avoids propagating invalidation through parts of an application that may never be observed again. Many writes can occur before a reader next runs; one comparison then discovers that reconsideration is needed. If an intermediate derived value recalculates to the same value, its own change count can remain unchanged and unnecessary downstream calculation can stop there.

Why Predica applications do not normally need async/await

async/await starts with a sequential invocation, allows it to discover an unavailable result partway through execution, saves its instruction position and local variables, and resumes it when the result arrives. That solves the problem of preserving an unfinished invocation without occupying its original thread.

Predica's application model does not create that problem. An opportunistic evaluation handles pending or partial input and completes. A strict calculation is started only when its required inputs are available. Later UI and query evaluations use their own snapshots and current contexts. There is no unfinished application invocation that ought to be resumed.

Preserving such an invocation would often preserve exactly the wrong state: values calculated from an older snapshot, an earlier viewport, a superseded selection, or demand that no longer exists. The information system should publish newly available facts and allow current readers to decide what those facts now mean.

This model also avoids dividing application functions into synchronous and asynchronous kinds. A derived attribute that navigates to database objects is written like any other derived attribute. Its accesses may cause I/O, shared calculation, or cache validation beneath the application model, while its own evaluation remains ordinary synchronous code.

Applications whose purpose is to view or edit data naturally fit this model. Editors, browsers, email clients, games, database applications, interactive command-line programs, and visualisation tools all repeatedly derive a current presentation from changing and partially available information. Their central operation is a fresh interpretation of current state, not continuation of an old interpretation that once encountered missing data.

The guiding rule is therefore:

Persist information, demand, and dependency evidence—not suspended intention.

Predica application code remains synchronous while database access and derived calculation are effectively asynchronous at every point where information may not yet be available.