Flat Map

Gather every matching value from a list of things, and be honest about what “every” means.

flat-map.py · output · proof · check · try it in the playground


The question

Imagine a small table of facts, each saying subject, property, value: a book has an author, a person has a phone number. Some subjects have one value for a property, some have several, some have none.

We want to take a list of subjects and collect all their values for one property into a single flat list, in order. Programmers call this a flat map. What happens with a subject that has two values? One that has none? A property nobody uses?

For a list of subjects, what are all their values for one property?


What we tell eyekind

fact(t('s1', 'p1', 'o1'))
fact(t('s2', 'p1', 'o2'))
fact(t('s3', 'p1', 'o3'))
fact(t('s3', 'p1', 'o4'))
# append(Xs, Ys, Zs): Zs is Xs followed by Ys.
fact(append([], Ys, Ys))
implied_by(append([X, *Xs], Ys, [X, *Zs]), append(Xs, Ys, Zs))
# flat_map(Subjects, Predicate, Objects)
fact(flat_map([], _, []))
implied_by(
  flat_map([S, *Subjects], P, Objects),
  [findall(O, t(S, P, O), Here), flat_map(Subjects, P, Rest), append(Here, Rest, Objects)],
)
query(flat_map(['s1', 's2', 's3'], 'p1', Objects))
query(flat_map(['missing'], 'p1', Objects))
query(flat_map(['s1'], 'p2', Objects))

What eyekind concludes

// Answers: each instance of a query() that holds.
flat_map(['s1', 's2', 's3'], 'p1', ['o1', 'o2', 'o3', 'o4'])
flat_map(['missing'], 'p1', [])
flat_map(['s1'], 'p2', [])

The proof, line by line

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

// Answers: each instance of a query() that holds.
flat_map(['s1', 's2', 's3'], 'p1', ['o1', 'o2', 'o3', 'o4'])
flat_map(['missing'], 'p1', [])
flat_map(['s1'], 'p2', [])

// Clauses: the program clauses the steps cite, by clause number.
clause(5, fact(append([], Ys, Ys)))
clause(6, implied_by(append([X, ...Xs], Ys, [X, ...Zs]), append(Xs, Ys, Zs)))
clause(7, fact(flat_map([], _0, [])))
clause(8, implied_by(flat_map([S, ...Subjects], P, Objects), [findall(O, t(S, P, O), Here), flat_map(Subjects, P, Rest), append(Here, Rest, Objects)]))

// 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(flat_map(['s1', 's2', 's3'], 'p1', ['o1', 'o2', 'o3', 'o4']), clause(8), {S: 's1', Subjects: ['s2', 's3'], P: 'p1', Objects: ['o1', 'o2', 'o3', 'o4'], O: A, Here: ['o1'], Rest: ['o2', 'o3', 'o4']}, [findall(A, t('s1', 'p1', A), ['o1']), flat_map(['s2', 's3'], 'p1', ['o2', 'o3', 'o4']), append(['o1'], ['o2', 'o3', 'o4'], ['o1', 'o2', 'o3', 'o4'])])
step(findall(A, t('s1', 'p1', A), ['o1']), 'collected', {}, [])
step(flat_map(['s2', 's3'], 'p1', ['o2', 'o3', 'o4']), clause(8), {S: 's2', Subjects: ['s3'], P: 'p1', Objects: ['o2', 'o3', 'o4'], O: B, Here: ['o2'], Rest: ['o3', 'o4']}, [findall(B, t('s2', 'p1', B), ['o2']), flat_map(['s3'], 'p1', ['o3', 'o4']), append(['o2'], ['o3', 'o4'], ['o2', 'o3', 'o4'])])
step(findall(B, t('s2', 'p1', B), ['o2']), 'collected', {}, [])
step(flat_map(['s3'], 'p1', ['o3', 'o4']), clause(8), {S: 's3', Subjects: [], P: 'p1', Objects: ['o3', 'o4'], O: C, Here: ['o3', 'o4'], Rest: []}, [findall(C, t('s3', 'p1', C), ['o3', 'o4']), flat_map([], 'p1', []), append(['o3', 'o4'], [], ['o3', 'o4'])])
step(findall(C, t('s3', 'p1', C), ['o3', 'o4']), 'collected', {}, [])
step(flat_map([], 'p1', []), clause(7), {_0: 'p1'}, [])
step(append(['o3', 'o4'], [], ['o3', 'o4']), clause(6), {X: 'o3', Xs: ['o4'], Ys: [], Zs: ['o4']}, [append(['o4'], [], ['o4'])])
step(append(['o4'], [], ['o4']), clause(6), {X: 'o4', Xs: [], Ys: [], Zs: []}, [append([], [], [])])
step(append([], [], []), clause(5), {Ys: []}, [])
step(append(['o2'], ['o3', 'o4'], ['o2', 'o3', 'o4']), clause(6), {X: 'o2', Xs: [], Ys: ['o3', 'o4'], Zs: ['o3', 'o4']}, [append([], ['o3', 'o4'], ['o3', 'o4'])])
step(append([], ['o3', 'o4'], ['o3', 'o4']), clause(5), {Ys: ['o3', 'o4']}, [])
step(append(['o1'], ['o2', 'o3', 'o4'], ['o1', 'o2', 'o3', 'o4']), clause(6), {X: 'o1', Xs: [], Ys: ['o2', 'o3', 'o4'], Zs: ['o2', 'o3', 'o4']}, [append([], ['o2', 'o3', 'o4'], ['o2', 'o3', 'o4'])])
step(append([], ['o2', 'o3', 'o4'], ['o2', 'o3', 'o4']), clause(5), {Ys: ['o2', 'o3', 'o4']}, [])
step(flat_map(['missing'], 'p1', []), clause(8), {S: 'missing', Subjects: [], P: 'p1', Objects: [], O: D, Here: [], Rest: []}, [findall(D, t('missing', 'p1', D), []), flat_map([], 'p1', []), append([], [], [])])
step(findall(D, t('missing', 'p1', D), []), 'collected', {}, [])
step(flat_map(['s1'], 'p2', []), clause(8), {S: 's1', Subjects: [], P: 'p2', Objects: [], O: E, Here: [], Rest: []}, [findall(E, t('s1', 'p2', E), []), flat_map([], 'p2', []), append([], [], [])])
step(findall(E, t('s1', 'p2', E), []), 'collected', {}, [])
step(flat_map([], 'p2', []), clause(7), {_0: 'p2'}, [])

It has three parts:

Read the steps as sentences:

  1. The flat map of s1, s2, s3 is ['o1', 'o2', 'o3', 'o4'] — by rule 8: the values of s1 are ['o1'], the flat map of the rest is ['o2', 'o3', 'o4'], and joining them gives the answer.
  2. All p1 values of s1 are ['o1'] — collected: everything eyekind found. Taken on trust as complete (see below).
  3. The flat map of s2, s3 is ['o2', 'o3', 'o4'] — by rule 8, in the same way.
  4. All p1 values of s2 are ['o2'] — collected.
  5. The flat map of s3 alone is ['o3', 'o4'] — by rule 8, with nothing left after s3.
  6. All p1 values of s3 are ['o3', 'o4'] — collected.
  7. The flat map of no subjects is [] — fact 7.
  8. ['o3', 'o4'] followed by [] is ['o3', 'o4'] — by rule 6, keeping o3 in front.
  9. ['o4'] followed by [] is ['o4'] — by rule 6.
  10. [] followed by [] is [] — fact 5.
  11. ['o2'] followed by ['o3', 'o4'] is ['o2', 'o3', 'o4'] — by rule 6.
  12. [] followed by ['o3', 'o4'] is ['o3', 'o4'] — fact 5.
  13. ['o1'] followed by ['o2', 'o3', 'o4'] is the whole list — by rule 6.
  14. [] followed by ['o2', 'o3', 'o4'] is that list — fact 5.
  15. The flat map of missing is [] — by rule 8: it has no values, nothing follows it, and [] followed by [] is [] (steps 7 and 10, reused).
  16. All p1 values of missing are [] — collected: none were found.
  17. The flat map of s1 for p2 is [] — by rule 8, the same way.
  18. All p2 values of s1 are [] — collected.
  19. The flat map of no subjects for p2 is [] — fact 7.

Why do no steps cite the t facts? A 'collected' step stands for the whole search behind a findall. The part that matters, “there are no other values”, can only come from having searched everything. A proof cannot point at a missing value, so this step is taken on trust rather than proved.


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:

// Answers checked subject to the obligations below.
flat_map(['s1', 's2', 's3'], 'p1', ['o1', 'o2', 'o3', 'o4'])
flat_map(['missing'], 'p1', [])
flat_map(['s1'], 'p2', [])

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

// Obligations: absence and collection completeness are taken on trust.
obligation('collected', 'theory_scoped', findall(A, t('s1', 'p1', A), ['o1']))
obligation('collected', 'theory_scoped', findall(A, t('s2', 'p1', A), ['o2']))
obligation('collected', 'theory_scoped', findall(A, t('s3', 'p1', A), ['o3', 'o4']))
obligation('collected', 'theory_scoped', findall(A, t('missing', 'p1', A), []))
obligation('collected', 'theory_scoped', findall(A, t('s1', 'p2', A), []))

// Proof statistics: recorded steps and how they were checked.
steps(19)
verified(14)
recomputed(0)
composed(0)
trusted(5)
claims(3)

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

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

The statistics: 19 steps, 14 verified against the program, 5 trusted, 3 claims.

The five obligation lines are the five collections taken on trust: for example that s3 has exactly ['o3', 'o4'] and that missing has nothing at all. 'theory_scoped' means “complete with respect to this program”: if someone adds a fact, these lists could change, and the report says exactly which ones. The verdict is checked with obligations: the three answers follow from the program, provided those lists are complete. The verdict does not say the program has no other conclusions. A stricter check, --strict-proof, rejects any proof that leans on such trust; this one would fail it on exactly these 5 lists.


Try it

eyekind examples/flat-map.py
eyekind --goal "flat_map(['s3', 's1'], 'p1', Objects)" examples/flat-map.py

The second gives ['o3', 'o4', 'o1']: the order follows the subject list. Or open it in the playground.

Add the fact fact(t('s2', 'p1', 'o5')) and run again: the first answer becomes ['o1', 'o2', 'o5', 'o3', 'o4'].


Takeaway

“Here are all the matches” is a stronger statement than “here are some matches”. eyekind gives you the answer and marks precisely where it relied on having seen everything — so you know what to recheck when the data grows.