32 Universal Navigation
Ordinary navigation reaches values that can be reached from at least one current value. Universal navigation reaches values that can be reached from every current value.
For example, the following expression reaches the teaching days used by every advanced course:
u.course.[advanced]⇒session.time.day
A day is output only if each advanced course has some session on that day. The
left side, u.course.[advanced], supplies the courses being tested. The right
side, session.time.day, is tested from each of those courses.
The logical shape is:
day=D:Day
where
∀C:Course ∀A:Boolean
(u.course(C) ∧ C.advanced(A) ∧ A)
→ ∃S:Session ∃T:TimeSlot
C.session(S)
∧ S.time(T)
∧ T.day(D)
For every advanced course C, the right-hand path must be able to reach the
output day D.
Ordinary navigation versus universal navigation
Changing ⇒ to . changes the meaning:
u.course.[advanced].session.time.day
This reaches days used by at least one advanced course. The universal version:
u.course.[advanced]⇒session.time.day
reaches only days used by all advanced courses. Ordinary navigation is like a union of what each current value can reach. Universal navigation is like an intersection of what each current value can reach.
Asking whether all values satisfy a condition
Often a universal requirement is easiest to write by looking for counterexamples. This expression reaches courses that have no assessment worth more than 60 percent:
u.course.[assessment.[weight > 60].Σempty]
The inner filter reaches the bad assessments. Σempty then says that there are
none. This pattern is often the clearest way to say "all reached values satisfy
the condition".
Using folds and subset tests
Another common pattern is to collect reached values with .Σ, then compare sets.
For example, this expression reaches advanced courses whose prerequisites are all
mathematics courses:
u.course.[advanced].[
prerequisite.Σ ⊆ u.department.[name = "Mathematics"].course.Σ
]
Here the requirement is naturally a set inclusion. The left side is the set of prerequisites of the current course. The right side is the set of courses offered by the mathematics department.
Relation to relational division
Universal navigation is the path-expression form of division-style queries. A division query
asks for the values related to all values in some divisor relation. In the first example, the
divisor is the advanced courses, and the result contains days related to every one
of those courses by the session.time.day path.
The classic supplier-parts query "suppliers who supply all parts" has exactly this shape:
In Tutorial D, it can be written using non-existence:
S WHERE NOT EXISTS (
P WHERE NOT EXISTS (
SP WHERE SP.S# = S.S#
AND SP.P# = P.P#
)
)
It can also be written using DIVIDEBY:
S MATCHING (
SP { S#, P# }
DIVIDEBY P { P# }
PER ( S { S# } )
)
In Predica, universal navigation gives the same query directly:
s.part⇒(shipment.supplier)
The left side supplies the required parts. The right side reaches the suppliers for each part. The output suppliers are those reached from every part.
Adding a filter gives the usual "suppliers who supply all red parts" query:
In Tutorial D, the non-existence form is:
S WHERE NOT EXISTS (
(P WHERE COLOR = 'Red')
WHERE NOT EXISTS (
SP WHERE SP.S# = S.S#
AND SP.P# = P.P#
)
)
The corresponding DIVIDEBY form is:
S MATCHING (
SP { S#, P# }
DIVIDEBY (P WHERE COLOR = 'Red') { P# }
PER ( S { S# } )
)
In Predica, the filter is part of the left side of the universal navigation:
s.part.[color = Color.red]⇒(shipment.supplier)
The DIVIDEBY versions make the relation to relational division explicit. In
practice, Tutorial D users will often prefer the non-existence form: choose the
candidate values for which there does not exist a required value with no matching
relationship. Universal navigation packages that double-negation pattern into a
path operator.
Choosing the universal pattern
There are three common ways to express universal requirements. The best choice usually depends on the sentence you are trying to write.
| Use | When the thought is | Example |
|---|---|---|
Σempty |
there are no counterexamples | assessment.[weight > 60].Σempty |
a.Σ ⊆ b.Σ |
everything reached by a is among the things reached by b |
|
⇒ |
find the values reachable from every value on the left | u.course.[advanced]⇒session.time.day |
→ |
filter values by saying whenever this condition holds, that condition must hold | |
The difference is that Σempty tests for absence, a.Σ ⊆ b.Σ compares two
collected sets, ⇒ produces common reachable values, and → states a
conditional requirement.
Using → inside a filter often makes universal quantification appear when the
frame condition is written out. The implication itself is the condition, but
the witnesses used by its antecedent are usually universally closed when the
filter is rendered. This is why:
u.course.[assessment → assessment.weight ≤ 60]
reads like "courses for which every assessment has weight at most 60". The same idea can also be written by looking for no counterexamples:
u.course.[assessment.[weight > 60].Σempty]
The two expressions approach the universal requirement from different directions. The first says what must follow from each reached assessment. The second says there are no reached assessments that break the rule.
Do not reach for it too soon
Universal navigation is powerful, but it is not the only way to write universal
requirements. Use ⇒ when the result should be values reachable from every
left-side value. Use Σempty when the clearest thought is "there are no bad
cases". Use .Σ with subset testing when the clearest thought is "this set is
contained in that set".