Expand description
Recognising the join a loop already contains.
docs/99-the-data-tier-means-of-combination.md
§99.6:
for x in xs:whose body containsmap_get(ys, k(x))is an equi-join […] Recognising the shape and emitting aJoininstead of a capturedFlatMapwould make27-review.beckandexamples/board.beckfaster with no edit to either program.
The cost this removes is not a constant. A per-element function that captured the accumulator is
a different function on every event, so crate::engine’s rebuild rule reapplies it to
every element — a nested-loop join with no index, re-run from scratch per event.
27-review.beck is the corpus program that has one, and it did not know it did.
§What is recognised, stated as the condition rather than as the shape
One map_get(m, k) inside the loop’s body, where
mreads only what the function captured — so the collection being looked up in is a node the plan already has, or can build, rather than something derived per element; andkreads only the element — so the join key is a function of the left row alone, which is what makes it an equi-join rather than a predicate.
Both conditions are about which variables an expression reads, so both survive the lookup being
written behind a call: 27-review’s is three definitions deep (verdict_for → map_get), and
§99.6 forecast that as the case inference would fail on. It does not, because the body is
inlined before it is searched — but the limit is real and moved rather than removed, and
Refusal is where it is named.
§The second shape: a filter that is a lookup into an index nobody built
One filter_list(xs, lambda y: g(y) == k(x)) inside the loop’s body, where
xsreads only what the function captured, as above;greads only the filtered element, so it is a key the collection can be arranged by; andkreads only the loop’s element, so the probe is a function of the left row alone.
That is the same equi-join with a different right side. map_get’s collection is a Map whose
own key is the join key, so crate::plan::Op::MapValues’s arrangement already answers it and
at most one row comes back. A filter’s collection is keyed by something else entirely, so the
index has to be built — crate::plan::Op::ArrangeBy, §99.9 item 3 — and several rows share a
key, so what comes back is the group.
The group is the rows the predicate would have kept, in the order the collection held them,
because the index’s key is g(y) followed by the collection’s own key and the probe takes the
range under g(y). That the two agree at all is a fact about Prim::Eq rather than a
convention: == is crate::Value’s own total order compared for equality, which is the order
the arrangement is a BTreeMap in.
What this does not do, stated here because the operator’s name promises more. The group is a
list, because the expression it replaced was one and its consumer loops over it. So a row
added to a group rebuilds that group’s list and no other — the scan over the whole collection
is gone and the capture with it, but the group’s own size is still paid. Removing that is
group by (§99.9 item 6), which is why item 6 follows this one rather than standing beside it.
§The third shape, which is the second one asked a different question
list_len(filter_list(xs, lambda y: g(y) == k(x))) is the same equi-join again, and what differs
is only what a probe returns: a number rather than the rows. Nothing about the index changes, so
this is a field of the grouped shape (Answers) rather than a shape of its own — and it is the
first of §99.9 item 6’s aggregates, the one the language already had a spelling for. A group that
is only ever counted is never built, which is what crate::plan::Matching::Count is for.
§The fourth shape: a number the group’s rows decide
list_min(filter_list(…)), list_max(…) and list_sum(…), bare or over a map_list of the
same filter, are the same question once more — §99.9 item 6’s other three aggregates. What
differs from the count is that the answer is a function of what the rows say rather than only
of how many there are, so something has to hold what they contribute:
crate::plan::Op::GroupBy, keyed by the group and holding per group whatever its aggregate
needs — a multiset of the projection, whose two ends are min and max, or a running total,
which is sum.
It is the one shape whose index is not an index. The other three probe an arrangement of the
collection; this probes an arrangement of the groups, one entry each. For the extremes the
join above it is a crate::plan::Matching::Unique — the same probe a map_get gets, Some
for a group with rows and None for one without, which is what list_min returns of a list and
of an empty one. For a total it is a crate::plan::Matching::Total, and the difference is
what a missing entry means: list_sum of no rows is 0, so the probe answers with a value
where the extremes answer with an absence.
§The fifth shape, which is not a loop at all
filter_list(xs, lambda x: map_contains(m, k(x))) and its negation are the algebra’s
intersection and difference by key — crate::plan::Op::Restrict, §99.9 item 7 — and
restriction is where they are read. The conditions are the same two conditions again: m
reads only what the function captured, k reads only the element.
What differs is where the shape is looked for, and the reason is what comes out of the
operator. A join is recognised at a site inside a body, because a loop does other things
besides look up and the body has to be rewritten around the row. A restriction has no body to
rewrite: it keeps and drops the elements the filter was keeping and dropping, so the predicate
is not rewritten, it is deleted. That is also why a filter_list can have this operator when
it cannot have a join — a join’s element is a row, and a filter’s consumers read the element.
The cost it removes is the same one, arrived at from the other side. A predicate that reads a
collection is a different predicate whenever that collection moves, so crate::engine’s
rebuild rule reconsiders every element on every event — a nested-loop anti-join with no index.
Structs§
- Aggregate
- What a group was asked for, and what its rows contribute to the question.
- Lookup
- One lookup, as the join that answers it.
- Membership
- One membership test, as the restriction that answers it.
- Recognised
- A loop whose body looked things up, taken apart.
Enums§
- Answers
- What a body wanted from a group, which decides whether the group has to be built at all.
- Index
- The index a lookup is answered from — the one difference between the two shapes recognised.
- Refusal
- Why a body that contained a
map_getwas not recognised as a join.
Constants§
- LEFT
- The two halves of a joined row, as the field names the rewritten body reads them by.
- RIGHT
- ROW
- The type name the joined row carries. Nothing checks it — the plan runs after the checker — but
a value that prints as
Join(left=…, right=…)in a panic is worth the four bytes.
Functions§
- fingerprint
- An expression’s shape as a string, so two that are the same expression share one index.
- fingerprint_
fun - The same, for an expression written over one bound parameter, whose number is written canonically.
- recognise
- Try to read a loop’s per-element function as a join.
- restriction
- Try to read a filter’s predicate as a membership test against another collection.