8  Four words, two languages

None of this is a package yet, and the distinction matters more here than anywhere else in the book. There are now three implementations of these four words and not one of them ships: the Python prototypes in design/, a hand-written R re-implementation, and the Rust core. What is missing is the thin R and Python packages that would call the core — and until those exist, nobody can install any of it.

The three agree on every sentence in this chapter. That is a fact about the plumbing and not about the vocabulary, which is the distinction the end of this chapter is about.

8.1 rows — say what a row is, not where it lives

The hypothesis in one line: every existing tool makes you state paths, from which the row shape emerges as a side effect. rows makes you state what a row is.

rows(doc, "versions.*")
rows(doc, "versions.*")

* is “every key here, whatever it is called” — which is what makes it work on a document whose keys are data. The keys it matched come back beside the records, because on a keys-as-data document the key is the most important column and a tool that discards it has thrown away the version number.

8.2 first_present — a priority order, not a set

The first of these paths that is actually there.

first_present(node, "valueQuantity", "valueString", "valueCodeableConcept",
              default = NULL)
first_present(node, "valueQuantity", "valueString", "valueCodeableConcept",
              default=None)

Both halves of the name carry something people get wrong. first says the arguments are a priority order rather than a set. present says the only value skipped is a missing one — so a zero comes back.

This is the first word shared with the sibling project, where it is the same idea over a table rather than a document. Sharing a word across two projects needs a stated owner and a test on both sides from the day it is shared, or the two drift and the drift is invisible.

8.3 take — one row per record, one column per path

take(records, "name", "author.email", "dist.tarball")
take(records, "name", "author.email", "dist.tarball")

Nothing else is built, and that is the whole point. The measured cost claim is that building only the columns you asked for skips 97.9% of the cells a general-purpose flattener would materialise.

8.4 where — find the value, not the path

The verb for a document you have never seen: every path whose value matches, folded.

where(doc, "url")
where(doc, "url")

Folded, because otherwise it is the O(data) failure arriving through the front door — 5,511 matching paths in one corpus file, which is a wall of text rather than an answer. It reports the shapes instead.

8.5 The claim, checked

The two implementations were written independently and are diffed on real documents. This is the harness running now:


  parity: 19 sentences, two independent implementations

    01-npm-registry      versions.*                 py    288  R     288  keys 1.0.0           ok
    01-npm-registry      versions.*.dependencies.*  py  4,645  R   4,645  keys 1.0.0|connect   ok
    02-hn-thread         children.*                 py     25  R      25  keys 0               ok
    02-hn-thread         children**                 py    335  R     335  keys 0               ok
    03-natural-earth     features.*                 py    241  R     241  keys 0               ok
    05-fhir-bundle       entry.*.resource           py    564  R     564  keys 0               ok
    15-github-issues     *                          py    100  R     100  keys 0               ok
    16-movie-ratings     *.*                        py     38  R      38  keys 0|12 Strong     ok
    17-openlibrary       docs.*                     py    200  R     200  keys 0               ok
    19-chicago-salaries  *                          py  5,000  R   5,000  keys 0               ok
    05-fhir-bundle       .                          py      1  R       1  keys -               ok

    01-npm-registry      where(url)                 py    806  R     806   10 shapes  MISMATCH
      py (10, 806, 'versions.<key>.dist.tarball|288')
      R  (7, 806, 'versions.<key>.<key>.url|325')
    01-npm-registry      where(email)               py  3,389  R   3,389    8 shapes  MISMATCH
      py (8, 3389, 'versions.<key>.contributors.[].email|1532')
      R  (11, 3389, 'versions.<key>.<key>.[].email|2370')
    02-hn-thread         where(date)                py    336  R     336   13 shapes  ok
    05-fhir-bundle       where(url)                 py  5,511  R   5,511   42 shapes  MISMATCH
      py (42, 5511, 'entry.[].resource.item.[].adjudication.[].category.coding.[].code|1104')
      R  (44, 5511, 'entry.[].resource.<key>.[].adjudication.[].category.coding.[].code|1104')
    07-graphql-introspection where(empty)               py  3,461  R   3,461   43 shapes  ok
    10-wikidata          where(url)                 py  1,583  R   1,583   10 shapes  MISMATCH
      py (10, 1583, 'entities.Q30.claims.<key>.[].references.[].snaks.<key>.[].datavalue.value|700')
      R  (22, 1583, 'entities.Q30.claims.<key>.[].references.[].snaks.P854.[].datavalue.value|679')
    13-package-lock      where(url)                 py  1,996  R   1,996    5 shapes  ok
    16-movie-ratings     where(empty)               py      0  R       0    0 shapes  ok

  15 of 19 agree

8.6 Why a second implementation was written at all

This looks like exactly the mistake the architecture forbids. One Rust core, so that two hand-written copies can never drift — and here are two hand-written copies.

The distinction is that one of them is a probe rather than a product, and it answers a question the core cannot:

One core makes the two languages agree by construction. That proves the plumbing. It cannot tell you whether the vocabulary is right. If rows("versions.*") reads naturally in Python and awkwardly in R, a shared core delivers the awkward reading to R users faithfully and forever. The port does not test that assumption, it locks it in.

The only way to test it is to have the words re-derived from the written notation by another route, and see whether they land in the same place. They did, on every sentence above.

Two hand-written copies must never be shipped, because they drift invisibly. Writing one as a probe and throwing it away costs nothing and buys the one piece of evidence the architecture assumes.

8.6.1 The core now answers these sentences, and the R copy has not been deleted

The plan said design/fathom.R goes the day the core answers them. It does — rows and where in Rust land on the same figures as both the Python and the R, on all nineteen.

It is kept anyway, and the reason is worth more than the rule. The same plan keeps the parity harness by retargeting it from two implementations to two bindings of one core. There are no bindings yet. Deleting the R today would break that harness and leave the core checked against one implementation where it is currently checked against two — trading a working check for nothing, to be tidy.

It goes when the R package exists to replace it. Written down as a deliberate deferral, because a plan followed to the letter at the cost of the thing it was protecting is not being followed at all.