!standard 4.5.1 09-10-28 AI05-0176-1/01
!class Amendment 09-10-28
!status work item 09-10-28
!status received 09-10-28
!class amendment
!status received
!priority Low
!difficulty Easy
!subject Quantified expressions
!summary
Syntax is proposed for universally quantified expressions over a finite domain.
!problem
It is very common in formal developement to define a predicate over a collection
to express the fact that all elements of the collection satisfy a given
property. (This is of course borrowed from the language of set theory and fornal
logic). For example, the fact that an array A is sorted can be expressed by
stating that for all values of an index I in the range from A'First to A'Last -
1 inclusive, the property A(I) <= A (I+1) obtains. Mathematical usage is to
write, e.g. forall X | P (X), to indicate that all elemnts X satisfy the
property P (X). When using such notation, the context is supposed to indicate
the universe in which the elements X are to be found. This is of course not
usable in a programming language that is meant to be executable, and the syntax
must indicate the domain of definition of the element. The common notation is:
forall X in domain | P (X).
The domain can be a range, or an expression that designates a container. The
first is typically used when the collection is an array, and the index
(the bound variable in the expression) is used to denote an element of the
collection in the condition. For collections with no built-in ordering, the
bound variable denotes directly an element of the collection:
for all X in Directory | Name (X)'Length < 20
Directory may be one of the predefined containers, or an array.
Such expressions will prove very useful in pre/post conditions and invariants.
!proposal
Quantified expressions have the following syntax:
( for all defining_identifier in Domain | Boolean_Expression )
Domain => Discrete_Range | Expression
!wording
!discussion
Instead of a vertical bar, we could use a colon to separate the iterator from
the condition. The proposed notation is more common in mathematics.
When quantifying over an array, the syntax provides the index, i.e. a cursor
over the array. There is no obvious domain over which to define a cursor in the
case of a container, so the notation (for all X in C | P (X)) is natural.
However, for the case of Maps, it would be attractive to have a form that
binds both a key and the corresponding element in the container. This suggests
introducing an attribute 'Domain (we already have 'Range) so we could write
(for all Key in M'Domain ( Find (M, Key) > 7)
Given that a mapping has a domain and a range, this suggests generalizing both
of these attributes to be used in quantified expressions and in loops, but
this is probably too big a change at this time.
!example
** TBD **
--!corrigendum 4.5.1(0)
!ACATS test
Add an ACATS C-Test to test the syntax.
!appendix
From: Robert Dewar
Sent: Thursday, March 5, 2009 2:44 PM
...
And while we are at it, if we are talking useful stuff in PPC's, how about
adding quantifiers while we are at it
(for all X in Sint => Sqrt (X) < 23)
or something like that ...
****************************************************************
From: Ed Schonberg
Sent: Thursday, March 5, 2009 2:52 PM
Would mesh nicely with some new iterator forms, of course... There
is no concern here that the iteration might not finish.
****************************************************************
From: Ed Schonberg
Sent: Sunday, March 15, 2009 9:07 AM
> where lhs is the same (possibly complex) lhs. much cleaner and clearer
> to write
>
> lhs := (if bla then 3 else 4);
Agreed, this will be used abundantly. I like the default boolean True
interpretation when the else part is missing.
Not convinced about the need for case expressions, but would like to see a
similar proposal for what Randy calls loop expressions, i.e. quantified
expressions.
****************************************************************
From: Robert Dewar
Sent: Sunday, March 15, 2009 9:30 AM
The tricky issue in quantifiers is how to deal with iterators.
****************************************************************
From: Ed Schonberg
Sent: Sunday, March 15, 2009 9:41 AM
Of course, but the point is to find a very light syntax, so this is not
necessarily tied up to the full iterator proposal that Randy developed. To be
useful is should cover both arrays and containers with a similar syntax, and
should mention elements, not cursors, something like
(for all E in A : F (E) > 0)
If this is defined as a tampering context we can probably simplify semantics and
termination issues. Should this be short-circuited? etc.
****************************************************************
From: Robert Dewar
Sent: Sunday, March 15, 2009 10:20 AM
Definitely short circuited, this is a search for a counterexample and no point
in continuing once a counterexample has been found.
The question to me is does this only work with built in containers, or is there
a way to make your own containers pick up the iteraction.
Also do we have
(exists E in A : F(E) > 0)
con: another keyword
pro: a bit more transparent then
not (for all E in A : F(E) <= 0)
Also, are we sure : is right, I think | might be better
(for all E in A | F (E) > 0)
though I guess to some mathematicians it will read as such that, and perhaps we
should allow the full notation
(for all E in A | E > 0 : F (E) > 0)
read "for all E in A such that E > 0, it is the case that F (E) > 0"
BTW, an implementation is allowed and perhaps encouraged to allow the forall
symbol to replace for all as a representation alternative. Could even require it
(surely a better use of invading mysterious characters in the language proper
than pi in the numerics stuff!)
****************************************************************
From: Bob Duff
Sent: Sunday, March 15, 2009 9:46 AM
...
>...but would like to
> see a similar proposal for what Randy calls loop expressions, i.e.
> quantified expressions.
I've no idea what quantified expressions should look like or mean, so I too
would like to see a proposal. Didn't Cyrille volunteer to send one to
ada-comment?
****************************************************************
From: Edmond Schonberg
Sent: Monday, March 16, 2009 2:08 PM
> I've no idea what quantified expressions should look like or mean, so
> I too would like to see a proposal. Didn't Cyrille volunteer to send
> one to ada-comment?
I'm sure you do, you just forgot! Quantified expressions are first- order
formulae over sets (that is to say any kind of container). They are either
universal (using the forall quantifier) or existential (using the exists
quantifier). They use a bound variable to indicate some property of elements
of the set. So for example : forall X in S | P (X)
which is True is all elements of S satisfy property P. The existential
quantifier is redundant, because we can always rewrite:
Exists X in S | P (X)
as
not (forall X in S | not P (X))
but it' cheap and nicer to have both quantifiers.
The run-time semantics of such a construct are short-circuited, i.e.
the implicit iteration stops as soon as an example has been found.
The necessary constituents are a container and a predicate. The scope of the
bound variable is the body of the predicate. Now figure out the best syntax for
this, preferably one that feels natural in Ada!
****************************************************************
From: Randy Brukardt
Sent: Monday, March 16, 2009 2:17 PM
> I'm sure you do, you just forgot! Quantified expressions are
> first- order formulae over sets (that is to say any kind of
> container). They are either universal (using the forall
> quantifier) or existential (using the exists quantifier).
> They use a bound variable to indicate some property of elements of the
> set.
Could you translate this into English? This sounds like some philosophy class
run amok. :-)
(Specifically, what do you mean by "universal" and "existential" here??)
****************************************************************
From: Edmond Schonberg
Sent: Monday, March 16, 2009 2:36 PM
Now now, just look at the examples. A universally quantified
expression says: all elements of this set have this particular
property.
An existentially quantified one says : there does exist one element of this
set that satisfies this property . Both of these are in fact loops over a
set, and their naive implementation is:
forall := false;
Elmt := First_Element (S);
while Present (Elmt) loop
if not P(X) then
forall := False;
exit;
end if;
Next_Elmt (Elmt);
end loop;
And the converse for Exists. This conventional interpretation means that a
universally quantified expression over a null set is always true (all the
elements of the empty set are green, for example).
The rest is syntax. Conventional mathematical notation is Forall
(X) (P (X)) which is tricky to implement because it doesn't say
where X comes from (the domain is implicit in the condition itself).
For an executable programming language you want to specify the domain
explicitly, and unless you have lazy evaluation you want the domain to be
finite. A number of years ago SETL chose the notation
(forall X in S | P(X)). Instead of the vertical bar I've seen
suggestions for a colon, maybe closer to existing conventions in C- languages.
****************************************************************
From: Randy Brukardt
Sent: Monday, March 16, 2009 3:01 PM
Thanks for the clear explanation. This one I understand (and probably use from
time-to-time, I just didn't know it had a name).
****************************************************************
From: Robert Dewar
Sent: Monday, March 16, 2009 2:39 PM
> Could you translate this into English? This sounds like some
> philosophy class run amok. :-)
No, more like a review of a very elementary math course :-)
> (Specifically, what do you mean by "universal" and "existential"
> here??)
Well here I can't follow Randy's I-dont-know-no-stinking-math philosophy
universal and existential quantifiers seem totally familiar and natural to me.
****************************************************************
From: Randy Brukardt
Sent: Monday, March 16, 2009 2:59 PM
> No, more like a review of a very elementary math course :-)
Sorry, Robert, the last formal math course I took was 29 years ago. Unless I've
had reason to use the information in my day-to-day work, I've almost certainly
forgotten it.
> > (Specifically, what do you mean by "universal" and "existential"
> > here??)
>
> Well here I can't follow Randy's I-dont-know-no-stinking-math
> philosophy universal and existential quantifiers seem totally familiar
> and natural to me.
I think it is the terms "universal quantifiers" and "existential quantifiers"
here that threw me; Ed explains an "if all" and "if exist" (that's the backwards
E if I remember right) relationship. That I understand; I still don't remember
ever hearing them given these names. Sorry if that offends your sense of math
correctness (a version of political correctness, I think).
****************************************************************
From: Robert Dewar
Sent: Monday, March 16, 2009 4:33 PM
> I think it is the terms "universal quantifiers" and "existential
> quantifiers" here that threw me; Ed explains an "if all" and "if exist"
> (that's the backwards E if I remember right) relationship. That I
> understand; I still don't remember ever hearing them given these names.
See you *do* remember stuff from all that time ago :-)
> Sorry if that offends your sense of math correctness (a version of
> political correctness, I think).
Something like that no doubt, it's part of the secret handshake of
mathematicians :-)
****************************************************************
From: Tucker Taft
Sent: Monday, March 16, 2009 3:14 PM
Randy, I think you should probably drag out one of those old math textbooks if
we are going to be talking about preconditions and postconditions. Existential
(there exists) and universal (for all) quantifiers are pretty fundamental (as is
"implies" by the way ;-).
I think it is healthy for us to reexamine whether these standard logic notions
are appropriate for a programming language (at least you convinced me that
"implies" doesn't quite work for our purposes), but we should at least be able
to use the vocabulary of first-order logic in our *discussions*.
There are a lot of good books out there, and of course there is Wikipedia, which
probably has all you need. A good starting point might be:
http://en.wikipedia.org/wiki/First-order_logic
One book I have used is:
Software Engineering Mathematics
by Jim Woodcock and Martin Loomes
SEI series in Software Engineering, 1988.
This one is clearly aimed at programmers who want to get more "formal" in their
approach to software engineering. It also happens to use "Zed" notation (simply
"Z" in the UK), which is pretty popular among the formal methods crowd.
****************************************************************
From: Randy Brukardt
Sent: Monday, March 16, 2009 3:44 PM
> Randy, I think you should probably drag out one of those old math
> textbooks if we are going to be talking about preconditions and
> postconditions.
> Existential (there exists) and universal (for all) quantifiers are
> pretty fundamental (as is "implies" by the way ;-).
I agree that they are fundamental, I just had never heard these names before.
...
> I think it is healthy for us to
> reexamine whether these standard logic notions are appropriate for a
> programming language (at least you convinced me that "implies" doesn't
> quite work for our purposes), but we should at least be able to use
> the vocabulary of first-order logic in our *discussions*.
Not with me, sorry. I didn't take the logic class at UW, taking statistics and
probability instead. (I think the former may have been taken while I was still a
ChemE, so I had the credits when I transferred to a CS major.)
> There are a lot of good books out there, and of course there is
> Wikipedia, which probably has all you need. A good starting point
> might be:
>
> http://en.wikipedia.org/wiki/First-order_logic
OK, thanks for the reference. A quick look at this shows its going to take a lot
longer than a few minutes to make sense out of this. It'll have to wait a few
weeks at least, until ASIS and the minutes and the conference call are done.
[BTW, I think "first-order logic" is "and", "or", and "not". Anything more
complicated than that clearly has a higher order. ;-)]
****************************************************************
From: Edmond Schonberg
Sent: Monday, March 16, 2009 3:33 PM
> I think it is healthy for us to
> reexamine whether these standard logic notions are appropriate for a
> programming language (at least you convinced me that "implies" doesn't
> quite work for our purposes), but we should at least be able to use
> the vocabulary of first-order logic in our *discussions*.
I think the semantics of quantified expressions are intuitive and
straightforward to implement. They are particularly useful in pre- and
postconditions but have more general uses. The transformation of a quantified
expression into a loop is immediate, and also leads naturally to nested loops
when a condition is defined over elements of
two sets: (forall X in S1, Y in S2 : P (X, Y))
One nice thing about these implicit iterators is that they do not mention
cursors but speak directly about elements. I suppose that the expression can be
defined to be a tampering context, so that any perverse use of the condition to
modify an element will be rejected.
The more general question is whether this is too much machinery to add to the
language at this point.
****************************************************************
From: Tucker Taft
Sent: Monday, March 16, 2009 3:57 PM
The obvious alternative is to rely on programmers writing appropriate named
functions that do the iteration internally. I don't think forcing the
definition of named functions is appropriate for conditional expressions, but
for quantified expressions, I think expecting the programmer to program up an
appropriate named function is not too much to ask.
So I see these quantified expressions as significantly lower priority than
conditional expressions.
****************************************************************
From: Robert Dewar
Sent: Monday, March 16, 2009 4:38 PM
Note that in Setl you routinely write an expression which is pronounced
for all x in some-set such that p(x) is true, it is the
case that q (x) is also true.
that "such that" phrase is certainly convenient to have in practice, and the
mathematical crowd will be quick to point this out as a use of implies
forall X in some-set | p(x) -> q(x)
and in that context the -> reads pretty nicely I think but most likely
forall X in someset | (if p(x) then q(x))
the reason I like the such that phrase is that it is not so much that you want
to consider that somehow p(x) implies q(x), it is more like you only want to
consider the subset for which p(x) is true. This is logically equivalent, but
feels different, so a notation like
forall X in someset | p(x) : q(x)
reads clearer to me (though I think I would prefer to use the vertical bar for
the "it is the case that", rather than colon.
****************************************************************
From: Robert Dewar
Sent: Monday, March 16, 2009 4:41 PM
> The obvious alternative is to rely on programmers writing appropriate
> named functions that do the iteration internally. I don't think
> forcing the definition of named functions is appropriate for
> conditional expressions, but for quantified expressions, I think
> expecting the programmer to program up an appropriate named function
> is not too much to ask.
>
> So I see these quantified expressions as significantly lower priority
> than conditional expressions.
Well there is the issue of bodies in the spec, and it does not seem that you can
use renaming of expressions to achieve this if you don't have quantified
expressions.
I find the bodies-in-spec issue to be a significant one!
****************************************************************
From: Bob Duff
Sent: Monday, March 16, 2009 5:15 PM
> I'm sure you do, you just forgot! ...
Thanks for the mathematical logic lesson, but I think you misunderstood what I
meant. ;-)
I understand what universal quantifiers are in maths, and I haven't forgotten my
logic course (at least, I haven't forgotten the basics). I even taught this
stuff to Bill some time ago, and he understands, for example, why "all pink
unicorns are green" is true.
What I meant is that I don't know how this stuff should be translated into a
programming language, especially an imperative programming language like, say,
Ada. Given your recent comments, you are thinking that "for all" is just
syntactic sugar for a loop. No big deal But it sounded like Cyrille was
thinking of something more "math-y".
Maths doesn't have loops. Nor side effects. Nor "run time" vs. "compile time".
In maths we are comfortable talking about infinite sets, and combinatorially
huge sets.
A type (in Ada) has (or is?) a set of values. I was wondering if one can say
things like "for all values X, Y, and Z of type String, X >= Y and Y >= Z
implies X >= Z". This means all _possible_ values, not just all values that
some program happens to have computed so far. And a mathemetician would point
out that Natural'Last can be arbitrarily large, and would like to prove the
above for all possible implementations of Ada. But that's apparently not what
you mean by universal quantifiers in Ada.
****************************************************************
From: Robert Dewar
Sent: Monday, March 16, 2009 7:10 PM
Clearly the sets in this case have to be finite, and the notion of quantifiers
over finite sets is well established in mathematics. So, no you cannot have
non-effective quantifiers over infinite sets, you could expect to say;
(for all A in Integer | P (A));
but it would take a long time to compute this :-)
****************************************************************
From: Robert I. Eachus
Sent: Monday, March 16, 2009 8:39 PM
Let me try to bring this discussion out of the blue sky and back to the
practical. The primary goal of any extensions to Ada should be:to make programs
easier to write. As long as doing so does not make the programs more difficult
to read and understand, and as long as the extensions do not make it easier to
write bad (in some existential sense) programs than good programs. There are
going to be many other criteria about making the language easier or harder to
learn, to implement, or to prove correct. Plus to some extent a concern about
creating too big a language or a language that is not Ada.
So what would we expect to gain or lose from adding for all to Ada? We
currently have for subtypes:
for I in Subtype'Range loop...end loop;
The same syntax works for arrays:
for I in A'Range loop..end loop;
But for lists and other containers? Define what should be the body of the loop
as a procedure with a parameter of type Cursor, inside the body of the
procedure use calls to Element, and replace Element, or create more procedure
declarations, and finally call Iterate passing the container and the procedure.
In my humble opinion, the worst part of all that is not needing to know about
throwaway things like Cursors, but having to write often simple code out of
line.
declare
procedure Nonsense_Name(C: in Cursor) is
begin
if Element(C).Last_Modified = Today
then Update_Element(Container, C, Daily_Backup;
end if;
end Nonsense_Name;
begin
Iterate(Container; Nonsense_Name;
end;
-- if Daily_Backup has other parameters, I need another wrapper procedure.
Not very lightweight, especially if you use declare blocks to keep the wrapper
procedure close by. If you could pass a declare block in-line it would make
iterators much more comfortable to use and easier on readers as well.
(Implementation shouldn't be that hard, this is really syntactic sugar, with
access subprogram parameters already in the language.) Renaming Iterate: ;=)
For_All(Element, Container,
(if Element.Last_Modified = Today then Daily_Backup(Element); end if));
-- assumes that a declare block passed as a parameter would be wrapped
-- in parentheses, not declare..end
Is it worth stopping there, or is it worth going further? When Ada was first
being defined, I suggested defining a couple of "extra" operator names with no
associated operations. These could be used to name operations which didn't
match the standard set of operations. I don' t know if that ever got as far as
being thrown out in scope reductions in the 1983 or 1995 standards. But here
what we would really like is a template which takes two arguments and a block of
code:
for all Element in Container do
begin
if Element.Last_Modified = Today
then Daily_Backup(Element);
end if;
end;
There could be generic templates like this declared in Standard that could be
instantiated for (pairs of) types to create both for all and exists type
functionality. However, I think I am reaching well beyond what should be
considered for the next version of Ada. Besides, the huge improvement comes
from putting the sequence of statements in line, the syntactic sugar is pretty,
but doesn't add much.
The anonymous declare block parameters may be a winner, especially if it can be
merged with Randy's function renaming idea, by doing the same with renaming
expressions as functions:
function renames end [identifier];
Hmm. I think I see a problem, the first things I want to write are things like
Factorial or Ackermann's function:
function Factorial(N: Positive) return Integer
renames (if N = 0 then 1 else N * Factorial (N-1)); end Factorial;
function Ackermann(M, N: Positive) return Integer renames
(if M = 0 then N+ 1
elsif N = 0 then Ackermann(M-1, N)
else N * Ackermann (M-1, Ackermann(M,N-1)))
end Ackermann;
Should the recursive calls be disallowed (probably by making the name not
visible)? Or do we just accept that theorem provers can run into lots of cases
where the proof is possible, but takes a lot of CPU cycles? Certainly these
functions are well defined, even if it does make more sense to use a BigNum type
for the results. (For that matter it might be nice to add an Unbounded_Integer
type in the Numerics annex.)
Note that this example code for Ackermann, is really just syntactic sugar.
Well... I save three return keywords, and some semicolons in exchange for some
parentheses. I don't have a begin, but I do have a renames. The huge mental
savings come from combining the spec and body, and putting the body (hopefully)
where it is needed.
Looking back, my practical may look pretty blue sky to some of you, but don't
lose my point. Providing access to subprogram parameters makes a lot of things
more doabile in Ada, but in practice the "out lining" of the code becomes pretty
heavy. I try to use declare blocks to bring code back to almost where it should
be, but it is still pretty heavy for what should be simple operations. Randy's
renaming of expressions as functions has the same intended purpose, but in the
case of package specifications, declare blocks don't help.
****************************************************************
From: Randy Brukardt
Sent: Monday, March 16, 2009 9:03 PM
Proper iterators for Ada are discussed in AI05-0139-1, and were discussed at the
Tallahassee meeting.
Proper accessors for Ada are discussed in AI05-0142-1, on several recent threads
on Ada-Comment (esp. the one about light-weight subprogram definitions for
anonymous access types), and will be on the agenda of the upcoming subcommittee
meeting.
We're obviously interested in improving Ada in these areas. But it's not
particularly helpful to rehash all of the points that have already been covered
in these AIs and their e-mail threads.
****************************************************************
From: Edmond Schonberg
Sent: Monday, March 16, 2009 9:26 PM
[Editor's note: Forked back from AC-0180.]
> I agree. Large Boolean expressions are notoriously hard to read and
> debug.
I think we all agree on that. But to come back to the topic, quantified
expressions by themselves are compact and easy to read, and therefore are a
good choice to have in preconditions. Sortedness?
(for all I in A'first .. A'last -1 : A(I) <= A (I+1))
If the preconditions have a very complex description that requires them being
off-line, this may be a design issue, not a problem with boolean expressions per
se!
****************************************************************
From: Tucker Taft
Sent: Monday, March 16, 2009 10:54 PM
I think if we talk about object-oriented abstractions, then the "class-wide" /
"inheritable" preconditions are almost certainly going to be described using
(abstract) primitive functions, which are inevitably something you can't put in
the spec. E.g. for an abstract Stack, the precondition on "Pop(Stack)" is "not
Is_Empty(Stack)" and it is clear that Is_Empty will have to be defined
differently for each concrete extension of the abstract Stack type.
If we are talking about scalar or array-of-scalar abstractions I can see these
things being fully defined in the spec, but for an abstraction which is truly
"abstract" (either in the O-O sense or in the private-type sense), then the
predicates are themselves going to involve abstract/private functions. These
abstract predicates are in some sense "defined" by where they appear in
preconditions and postconditions. E.g., if the postcondition of "Push(Stack) is
"not Is_Empty(Stack)", and the precondition of "Pop(Stack)" is the same, then we
know that a Pop immediately following a Push is safe.
If we went further and defined a "Depth(Stack)" then we could get more specific,
such as having a postcondition for Push of "Depth(Stack) = Depth(Stack)'old + 1"
and a precondition on Pop of "Depth(Stack) > 0", and a postcondition of
"Depth(Stack) = Depth(Stack)'old - 1". Combined with a postcondition on
Depth(Stack) itself of "Depth'Result >= 0" then we can begin to understand the
stack-like nature of a Stack, without ever seeing the body for "Depth".
I believe that most "interesting" abstractions will have this characteristic,
that is, where it will be necessary to define "helper" functions to specify pre-
and postconditions, and the meaning of the helper functions will be implicit in
how they are used in pre- and postconditions (I'll admit that sounds circular),
and not require visibility on the body of these helper functions, some of which
may be "abstract" when talking about O-O type hierarchies.
Given this, I am dubious about investing heavily in quantified expression
syntax. Even for things like "sorted" I think you can't expect many compilers
to glean the truth of your "for all" expression from the code, but if you claim
via a postcondition of your sort routine that "Sorted(QSort'Result)" and
elsewhere you have a precondition of "Sorted(A)", then it would not be too much
of a leap for the compiler to expect you to have defined "A" by using QSort or
some other sorting routine, before calling a function that expected "Sorted(A)"
to be True.
To put this another way, I believe that from the *caller* side, pre- and
postconditions will be treated more "symbolically," where it is the matching up
of postconditions and preconditions which will be used to be sure that an object
is in the right "state" before being passed to some operation.
On the *callee* side, I believe the pre- and postconditions will actually be
analyzed in their full glory, because that is the place where they are
guaranteed to actually mean something, since that is where you have full access
to the representation, and in an O-O context, you actually know the concrete
type of the thing you are manipulating.
****************************************************************