demo D31
Pure versus impure computations
Why one is cached and offloadable and the other is not
value.pure
The configuration
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.13.0/lattice-grid.min.css">
<script src="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.13.0/lattice-grid.min.js"></script>
<div id="grid" style="height: 540px"></div>
<script>
// `value.pure` splits computations in two. A pure one depends only on its
// declared deps, so it can be cached and reused. An impure one reads
// something outside the row (a clock, a selection, a live rate), so in
// principle it is resolved again on each pass over the rows.
let pureCalls = 0;
let impureCalls = 0;
const loadedAt = Date.now();
const grid = LatticeGrid.createGrid(document.getElementById('grid'), {
rowKey: 'id',
toolPanel: {
side: 'left',
panels: ['columns', 'filters', 'views', 'quick'],
actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
exportName: 'lattice-demo',
},
columns: [
{ field: 'pipeline', title: 'Pipeline', filter: { type: 'set' }, layout: { width: 130, pin: 'start' } },
{ field: 'stage', title: 'Stage', filter: { type: 'set' }, layout: { width: 130 } },
{ field: 'elapsed', title: 'Elapsed', type: 'duration', layout: { width: 130 } },
{
id: 'perSecond', title: 'Records / sec', type: 'number', layout: { width: 150 },
format: { notation: 'compact' },
// The honest pure case: everything it reads is a declared dep, so
// caching it changes no answer.
value: {
deps: ['records', 'elapsed'], pure: true,
compute: (d) => (d.elapsed ? (d.records * 1000) / d.elapsed : null),
},
},
{
id: 'pureCall', title: 'Pure call #', type: 'number', layout: { width: 140 },
value: { deps: ['records'], pure: true, compute: () => ++pureCalls },
},
{
id: 'impureCall', title: 'Impure call #', type: 'number', layout: { width: 150 },
// Same counter idea, declared impure.
value: { deps: ['records'], pure: false, compute: () => ++impureCalls },
},
{
id: 'age', title: 'Value age', layout: { width: 150 },
// A figure whose answer depends on when you ask.
value: {
deps: ['records'], pure: false,
compute: () => `${Math.round((Date.now() - loadedAt) / 1000)}s after load`,
},
},
{ field: 'records', title: 'Records', type: 'number', layout: { width: 130 }, format: { notation: 'compact' } },
],
rows, // pipeline runs
});
// Force a refresh on a timer, so a reader can watch the impure columns.
setInterval(() => grid.rows.refresh({ force: true }), 1_000);
</script>
Marking a computed column pure so its value can be cached
A computed column derives its value from other columns in the row, and Lattice Grid needs to know whether that derivation is safe to cache and safe to move off the main thread. A pure computation reads only its declared inputs and returns the same output for the same inputs every time, with no reference to the current time, a random number, or anything outside the row. A developer reaches for value.pure whenever a derived column is expensive to recompute but cheap to look up again: a formatted address built from five fields, a risk score folded from a dozen numeric columns, a hash used for grouping. Lattice Grid sets this on the value definition, alongside deps, and once it is true the grid caches the result against its inputs and skips the recompute when nothing the column depends on has changed, and can schedule the work to a worker rather than the render path of a JavaScript data grid rendering thousands of rows. An impure computation, one that reads the wall clock, a global counter or anything mutable outside its declared inputs, cannot be cached or offloaded this way, because a stored result would go stale without any input changing, so the grid re-runs it inline on every pass that touches the row instead.
When should a computed column be marked pure versus left impure?
Mark it pure when the function only reads its declared deps and returns a stable result for the same inputs, which lets Lattice Grid cache the value and, where useful, run it off the main thread. Leave it impure when the function depends on anything else, such as the current time or an external counter, so the grid recomputes it on every relevant pass rather than serving a cached value that could be wrong.