Nested triple terms

A claim inside a review, and the fact inside the claim.

rdf12-nested-triple-term.py · output · proof · check · try it in the playground


The question

On the web, data is often written in RDF, a standard where every fact is a small sentence of three parts, a triple: subject, predicate, object, as in Bob — knows — Carol. Names are web addresses (IRIs) such as https://example.org/bob, so different sites can share them. The newest version, RDF 1.2, adds triple terms: a triple can be mentioned inside another one without being asserted as true.

Here an auditor reviews Alice’s claim that Bob knows Carol. “Bob knows Carol” sits two levels deep: inside Alice’s claim, which sits inside the auditor’s review. Nobody asserts that Bob knows Carol; the data only says that the auditor reviews a claim.

Can one rule reach the relationship two levels down and state it as a result?


What we tell eyekind

ex = 'https://example.org/'

fact(rdf(
  iri(ex + 'auditor'),
  iri(ex + 'reviews'),
  triple(iri(ex + 'alice'), iri(ex + 'claims'), triple(iri(ex + 'bob'), iri(ex + 'knows'), iri(ex + 'carol'))),
  'default_graph',
))

# A reviewed claim about who knows whom yields the relationship itself.
implies(
  rdf(_, iri(ex + 'reviews'), triple(_, iri(ex + 'claims'), triple(S, iri(ex + 'knows'), O)), G),
  result_rdf(S, iri(ex + 'knows'), O, G),
)
query(result_rdf(S, P, O, G))

RDF values are ordinary terms in eyekind:

fact(…) states the one piece of data: the auditor reviews (Alice claims (Bob knows Carol)). implies(pattern, conclusion) is a forward rule: whenever data matches the pattern, eyekind adds the conclusion. Capitalized S, O and G are variables, placeholders that take whatever values fit; a bare _ matches anything and its value is not used. query(…) asks for every result.

The pattern has the same nested shape as the data, so a single match reaches through both levels at once: someone reviews (someone claims (S knows O)).


What eyekind concludes

The innermost relationship, now stated as a result:

// Answers: each instance of a query() that holds.
result_rdf(iri('https://example.org/bob'), iri('https://example.org/knows'), iri('https://example.org/carol'), 'default_graph')

The first line is a comment saying what follows: the answers to the query. The one answer reads Bob knows Carol, in the default graph. Whether the auditor’s review should make that true is a choice the rule makes, and the rule is right there in the program for anyone to see.


The proof, line by line

eyekind --proof writes down how it reached the answer. This is the complete proof:

// Answers: each instance of a query() that holds.
result_rdf(iri('https://example.org/bob'), iri('https://example.org/knows'), iri('https://example.org/carol'), 'default_graph')

// Clauses: the program clauses the steps cite, by clause number.
clause(1, fact(rdf(iri('https://example.org/auditor'), iri('https://example.org/reviews'), triple(iri('https://example.org/alice'), iri('https://example.org/claims'), triple(iri('https://example.org/bob'), iri('https://example.org/knows'), iri('https://example.org/carol'))), 'default_graph')))
clause(2, implies(rdf(_0, iri('https://example.org/reviews'), triple(_1, iri('https://example.org/claims'), triple(S, iri('https://example.org/knows'), O)), G), result_rdf(S, iri('https://example.org/knows'), O, G)))

// Steps: each goal, what justifies it (a clause, a builtin, a control, or an
// absence or collection taken on trust), the bindings and the goals it uses.
step(result_rdf(iri('https://example.org/bob'), iri('https://example.org/knows'), iri('https://example.org/carol'), 'default_graph'), clause(2), {_0: iri('https://example.org/auditor'), _1: iri('https://example.org/alice'), S: iri('https://example.org/bob'), O: iri('https://example.org/carol'), G: 'default_graph'}, [rdf(iri('https://example.org/auditor'), iri('https://example.org/reviews'), triple(iri('https://example.org/alice'), iri('https://example.org/claims'), triple(iri('https://example.org/bob'), iri('https://example.org/knows'), iri('https://example.org/carol'))), 'default_graph')])
step(rdf(iri('https://example.org/auditor'), iri('https://example.org/reviews'), triple(iri('https://example.org/alice'), iri('https://example.org/claims'), triple(iri('https://example.org/bob'), iri('https://example.org/knows'), iri('https://example.org/carol'))), 'default_graph'), clause(1), {}, [])

It has three parts:

Read the steps as sentences:

  1. Bob knows Carol (as a result) — by rule 2, with _0 being the auditor, _1 being Alice, S being Bob, O being Carol and G being the default graph, because the auditor reviews Alice’s claim that Bob knows Carol.
  2. The auditor reviews Alice’s claim that Bob knows Carol — fact 1; it uses nothing.

The values in step 1 show how the single pattern lined up with the data, one level at a time.


Checked: what the report says

A proof is only worth something if someone else can check it. eyekind -c generates the proof and checks it against the program; --check-proof checks a saved proof. The checker never reasons by itself: it only confirms each step against the program. This is the complete report:

// Checked answers: these follow from the input program.
result_rdf(iri('https://example.org/bob'), iri('https://example.org/knows'), iri('https://example.org/carol'), 'default_graph')

// C1-C8: resolution, acyclicity, justification, coverage,
// re-decision, boundary consistency, relevance and witness integrity.
condition('C1', 'resolution', 'ok', 2)
condition('C2', 'well_founded', 'ok', 2)
condition('C3', 'justification', 'ok', 2)
condition('C4', 'coverage', 'ok', 2)
condition('C5', 're_decision', 'ok', 0)
condition('C6', 'boundary_consistency', 'ok', 0)
condition('C7', 'relevance', 'ok', 3)
condition('C8', 'witness_integrity', 'ok', 0)

// Proof statistics: recorded steps and how they were checked.
steps(2)
verified(2)
recomputed(0)
composed(0)
trusted(0)
claims(1)

// This verdict applies to the claims above; it does not establish completeness.
verdict('checked')

Each condition line names a check, says 'ok', and counts what it looked at:

The statistics repeat this: 2 steps, both verified against the program, none taken on trust. The verdict is checked: the result follows from the program. It confirms that result; it does not say there are no others.


Try it

eyekind examples/rdf12-nested-triple-term.py                # the results as quads
eyekind --proof examples/rdf12-nested-triple-term.py        # with their proof

Or open it in the playground.

Change 'carol' to 'dave' in the fact: the answer follows the change and says Bob knows Dave. Change Alice’s 'claims' to 'denies' instead: the pattern no longer matches, and eyekind reports // No answers: no query() instance holds.


Takeaway

Nesting is no obstacle: the shape of the data is the shape of the pattern.