The mathematical content of the definition of a recursive function is a
fixpoint equation. Let us demonstrate this statement with a simple example.
The textbook example of a recursive function is the factorial function. In our
programming language it looks like
factorial(n:NATURAL): NATURAL
ensure
Result = if n=0 then 1
else n*factorial(n-1)
end
end
The keyword Result
represents factorial(n)
i.e. the value of the function
at the argument n
. Since the postcondition is an equation the specification
of this function says
all(n:NATURAL)
ensure
factorial(n) = (if n=0 then 1 else n*factorial(n-1) end)
end
This specification states that the function factorial
and the function n ->
are the same function.
if n=0 then 1 else n*factorial(n-1) end
From the function factorial
we can extract the functional (for convenience
we call a function which accepts and returns functions a functional)
f(g:NATURAL->NATURAL): NATURAL->NATURAL
ensure
Result = (n -> if n=0 then 1 else n*g(n-1) end)
end
and then the specification of the function factorial
reads like
all(n:NATURAL)
ensure
factorial(n) = f(factorial)(n)
end
i.e. the function factorial
has to satisfy the fixpoint equation
factorial = f(factorial)
From the mathematical point of view two questions arise
f
have a fixpoint (i.e. existence)?f
unique?If the answer to both question is yes then we can say that the functional
f
defines uniquely a function.
In this article we study this question for primitive recursive functions over
natural numbers of the form
G: ANY
gr(n:NATURAL): G
ensure
Result = if n=0 then c -- c: constant (function)
else h(gr(n-1),n) -- h: total function
end
end
where the constant (function) c
and the function h:[G,NATURAL]->G
are well
defined. For the factorial function we have c=1
and h = ([a,b] -> b*a)
.
We restrict our investigation about primitive recursive functions in this
paper to primitive recursive functions on natural numbers.
We do not use a specific type NATURAL
but an abstract class
ABSTRACT_NATURAL
. This is convenient because we can have different
implementations of natural numbers. One possible implementation is to use a
machine word (e.g. 32 or 64 bit) to implement natural numbers or to use a
sequence of numbers to implement arbitrary sized natural numbers. Since the
basic properties and assertions of all implementations are identical it is
sufficient to derive them once in an abstract module.
In the first chapter we introduce the module abstract_natural
, give the
basic definitions, prove the induction law, show that there are infinitely
many natural numbers, introduce an order relation on the abstract natural
numbers and show that the order is a wellorder.
In the second chapter we introduce a general format of recursive functions and
show that typical recursive functions fit into this pattern.
In the third chapter we scrutinize the functional which represents a primitive
recursive function. We proof that the functional is monotonic, continuous and
has a unique fixpoint.
In our programming language we can use constants like 0
as names of
functions. In the class of natural numbers the constant 0
specifies the
number zero. In the class of sets 0
represents the empty set and 1
represents the universal set. In the class of partial functions (type A->B
where A
and B
are arbitrary types) the constant 0
represents the
completely undefined functions i.e. the function with an empty domain.
In this article the constant 0
appears with all these meanings. Usually it
is clear from the context whether the number zero, the empty set or the
undefined function is meant. Sometimes type annotations like 0:G?
are used
to annotate that 0
is of type G?
and represents therefore the empty set of
elements of type G
.
In the module abstract_natural
we want to use axioms which are equivalent to
the five peano axioms. The five peano axioms are:
Zero is a natural number
All natural numbers have successor
Zero is not the successor of a natural number
Two natural numbers which have the same successor are identical
Any property which is valid for the number zero and whose validity for any
number implies its validity for the successor of this number is valid for all
natural numbers.
The basic definition of the module abstract_natural
looks like:
deferred class
ABSTRACT_NATURAL
end
feature -- Basic functions and axioms
0: CURRENT -- peano 1
deferred end
succ(n:CURRENT): CURRENT -- peano 2
deferred end
1: CURRENT
deferred ensure Result = succ(0) end
all(n,m:CURRENT)
deferred
ensure
zero_not_succ: n.succ /= 0 -- peano 3
succ_injective: n.succ=m.succ => n=m -- peano 4
closed: (0).closed(succ) = (1:CURRENT?) -- peano 5
end
end
In order to represent the first peano axiom we use the constant function
0
. The second peano axiom is represented by the successor function
succ
.
The third and forth peano axiom are represented by the assertions
zero_not_succ
and succ_injective
.
The assertion closed
says that we can reach all natural numbers by starting
from the number 0
and applying the successor function succ
an arbitrary
number of times. It will be shown in the next chapter that the assertion
closed
is equivalent to the fifth peano axiom.
We are going to demonstrate that the assertion closed
is sufficient to prove
the classical induction law. Let us translate the fifth peano axiom
(induction) into our programming language
induction:
all(e:BOOLEAN)
require
r1: e[n:=0]
r2: all(n:CURRENT) e => e[n:=n.succ]
ensure
all(n:CURRENT) e -- r1,r2, induction_lemma (see below)
end
Because (0).closed(succ)
is a closure we get the general law
closure_induction_1:
all(p:CURRENT?)
require
0 in p
p.is_closed(succ)
ensure
(0).closed(succ) <= p -- See "Closures and fixpoints"
end
which have already been proved in the paper “Closures and fixpoints”. From
this law using closed
we can immediately derive the following assertion
closure_induction_2:
all(p:CURRENT?)
require
r1: 0 in p
r2: p.is_closed(succ)
check
c1: (0).closed(succ) <= p -- r1,r2,closure_induction_1
c2: (1:CURRENT?) <= p -- c1,closed
ensure
p = (1:CURRENT?) -- c2, 1 is greatest set
end
Now it is easy to derive the classical induction law.
induction_lemma:
all(e:BOOLEAN)
require
r1: e[n:=0]
r2: all(n:CURRENT) e => e[n:=n.succ]
local
l1: p:CURRENT? := {n: e}
check
c1: 0 in p -- l1,r1
c2: p.is_closed(succ) -- l1,r2
c3: p = (1:CURRENT?) -- c1,c2,closure_induction_2
c4: all(n:CURRENT) n in p -- c3
ensure
all(n:CURRENT) e -- l1,c4
end
We claim that the range of the successor function contains all natural numbers
except zero.
range:
all
ensure
succ.range = {n: n/=0} -- lemma_range
end
In order to convert this into a valid proof we have to prove that succ.range
and {n:n/=0}
are the same set. Two sets are the identical if they contain
the same elements i.e. we have to prove all(n) n in {n:n/=0} = n in
. Since this is a statement about all natural numbers we prove this
succ.range
assertion by induction (Note that n in {x:e} = e[x:=n]
i.e. n in {n:n/=0} =
).
n/=0
lemma_range:
all(n:CURRENT)
check
c1: 0 /in succ.range -- zero_not_succ
c2: (0/=0) = 0 in succ.range -- c1
c3: all(n:CURRENT)
require
(n/=0) = n in succ.range
check
c4: n.succ /= 0 -- zero_not_succ
c5: n.succ in succ.range -- trivial
ensure
(n.succ/=0) = n.succ in succ.range -- c4,c5
end
ensure
(n/=0) = n in succ.range -- c2,c3,induction
end
One of the immediate consequences of the basic definitions is that the set of
natural numbers must be infinite. We can see this by the following reasoning.
Since the successor function is defined as a total function (no preconditions)
its domain is the set of all natural numbers.
succ.domain = (1:CURRENT?)
In the previous chapter we have proved succ.range = {n:n/=0}
. This implies
that the range of the successor function is a proper subset of its domain.
succ.range < succ.domain
Because of the axiom succ_injective
the successor function is injective
succ.is_injective
From this we can conclude that the set of natural numbers is
infinite. Remember the definition of infiniteness of a set: A set p
is
infinite if there is an endofunction f
whose domain is the set p
and
whose range is a proper subset of its domain. The successor function is such a
function for the natural numbers.
I.e. we get
(0).closed(succ).is_infinite
and because of closed
infinite: (1:CURRENT?).is_infinite
Every injective function f
has an inverse function g=f.inverse
such that
f.domain=g.range
and f.range=g.domain
. The successor function is an
injective function. The range of the successor function is
{n:n/=0}
. Therefore the following predecessor function is uniquely defined.
pred(n:CURRENT): CURRENT
require
n /= 0
deferred
ensure
Result = (succ.inverse)(n)
end
We define an order relation on natural numbers by defining n<=m
if and only
if the number m
can be reached by starting at the number n
and applying
the successor function zero or more times.
<= (n,m:CURRENT): BOOLEAN
deferred
ensure
Result = m in n.closed(succ)
end
all(a,b:CURRENT)
ensure
(<=).is_reflexive -- consequence of closure
(<=).is_transitive -- consequence of closure
a<=b => b<=a => a=b -- lemma_anti (see below)
a<=b or b<=a -- lemma_linear (see below)
end
The reflexivity and transitivity of the relation <=
are direct consequences
of its definition as a closure. The proofs of the antisymmetry and the
linearity of <=
need a little bit more reasoning. In order to prove these
properties we need some definitions and laws given in the article
“Endofunctions, closures, cycles and chains”.
Let A
be any type.
is_connected(p:A?, f:A->A): ghost BOOLEAN
-- Is the set p connected under the function f?
ensure
Result = all(a,b)
require
{a,b}<=p
ensure
b in a.closed(f) or a in b.closed(f)
end
end
is_cycle(p:A?, f:A->A): ghost BOOLEAN
-- Does the set p form a cycle under f?
ensure
Result = some(a)
a.closed(f) = p
and a in f.domain
and a in f(a).closed(f)
end
has_cycle(p:A?, f:A->A): ghost BOOLEAN
-- Has the set p a subset which forms a cycle under f?
ensure
Result = some(q:A?) q<=p and q.is_cycle(f)
end
is_chain(p:A?, f:A->A): ghost BOOLEAN
-- Is the set p a chain under f?
ensure
Result = (p.is_closed(f)
and p.is_connected(f)
and not p.has_cycle(f))
end
With these definitions the following laws have been derived within the article
“Endofunctions, closures, cycles and chains”:
all(a:A, f:A->A)
ensure
connected: a.closed(f).is_connected(f)
infinite_chain:
a.closed(f).is_infinite => a.closed(f).is_chain(f)
end
Having these laws it is easy to prove the linearity of the order relation.
lemma_linear:
all(a,b:CURRENT)
check
c1: (0).closed(succ).is_connected(succ) -- connected
c2: b in a.closed(succ) or a in b.closed(succ)
-- c1, def is_connected
ensure
a<=b or b<=a -- c2
end
The antisymmetry of the order relation requires all(a,b) a<=b => b<=a =>
. We prove this law by contradiction and assume that
a=ba/=b
is
inconsistent with the assertions a<=b
and b<=a
. The inequality a/=b
implies that the set of natural numbers contained a cycle. This contradicts
the fact that the natural numbers form a chain which cannot contain a cycle.
lemma_anti:
all(a,b:CURRENT)
require
r1: a<=b
r2: b<=a
check
c1: require
r3: a/=b
check
c2: b in a.closed(succ) -- r1
c3: a in b.closed(succ) -- r2
c4: b in a.succ.closed(succ) -- c2,r3
c5: a in a.succ.closed(succ) -- c3,c4
c6: a.closed(succ).is_cycle(succ) -- c5
c7: a.closed(succ)<=(0).closed(succ) -- closed
c8: (0).closed(succ).has_cycle(succ) -- c6,c7
c9: (0).closed(succ).is_infinite -- infinite,closed
c10: (0).closed(succ).is_chain(succ) -- c9,infinite_chain
c11: not (0).closed(succ).has_cycle(succ)
-- c10,def chain
ensure
false -- c8,c11
end
ensure
a=b -- c1
end
We claim that the order relation is a wellorder i.e. each nonempty set p
has
a minimal element.
wellorder:
all(p:CURRENT?)
require
p /= 0
ensure
some(n) n.is_least(p)
end
Before proving this theorem we first prove the related theorem all(n:CURRENT)
which says that for
all(p:CURRENT?) p*{m:m<=n}/=0 => some(m) m.is_least(p)
all natural numbers n
and for all sets p
the nonemptiness of the
intersection of p
with the downward closure of n
implies that there is a
least element of p
. Since this is a statement about all natural number we
can use induction to proof it.
lemma_wo:
all(n:CURRENT, p:CURRENT?)
check
c1: require
r1: p*{m:m<=0} /= 0
check
c2: 0 in p -- r1
ensure
some(m) m.is_least(p) -- c2, witness 0
end
c3: all(n:CURRENT)
require
r2: p*{m:m<=n}/=0 => some(m) m.is_least(p)
r3: p*{n:m<=n.succ}/=0
check
c4: p*{m:m<=n}=0 or p*{m:m<=n}/=0
c5: require
r4: p*{m:m<=n}=0
check
c6: n.succ in p -- r3,r4
c7: n.succ.is_least(p) -- c6,r4
ensure
some(m) m.is_least(p) -- c7, witness n.succ
end
ensure
some(m) m.is_least(p) -- c4,c5,r2
end
ensure
p*{m:m<=n}/=0 => some(m) m.is_least(p)
end
Having this we can prove the wellorder theorem.
wellorder:
all(p:CURRENT?)
require
r1: p /= 0
check
c1: some(n:CURRENT) n in p
c2: some(n:CURRENT) p*{m:m<=n}/=0 -- c1
c3: all(n:CURRENT)
require
r2: p*{m:m<=n}/=0
ensure
some(m) m.is_least(p) -- r2, lemma_wo
end
ensure
some(n) n.is_least(p) -- c2,c3
end
Let us look at some primitive recursive functions on natural numbers. First we
can define addition of two natural numbers n+m
. We use a definition
which is recursive in the first argument (an equivalent definition being
recursive in the second argument is possible as well).
+ (n,m:CURRENT): CURRENT
-- The sum of the numbers n and m.
deferred
ensure
Result = if n=0 then m
else (n.pred+m).succ
end
end
Then we can define multiplication of two natural number n*m
.
* (n,m:CURRENT): CURRENT
-- The product of the numbers n and m.
deferred
ensure
Result = if n=0 then 0
else (n.pred*m) + m
end
end
As a third typical primitive recursive function we define n^m
which
represents the number n
raised to the m
-th power.
^ (n,m:CURRENT): CURRENT
-- The number n raised to the m-th power.
deferred
ensure
Result = if m=0 then 1
else
n^(m.pred) * n
end
end
Now we want to transform the definition of a recursive function into the
general form
gr(n:CURRENT): G
ensure
Result = if n=0 then c
else h(gr(n.pred),n)
end
end
where
c: CURRENT->G
h: [G,CURRENT]->G
Let us look at the function +
+ (n,m:CURRENT): CURRENT
deferred
ensure
Result = if n=0 then m -- (m->m)(m)
else (n.pred+m).succ -- (m->(n.pred+m).succ)(m)
end
end
to derive the transformation.
The function gr
is a function of one argument and the function +
has two
arguments. Therefore the result type G
of the function gr
has to be a
function of type CURRENT->CURRENT
so that n+m = gr(n)(m)
i.e. the function
gr
is the curried version of +
. If we uncurry the definition of gr
we
get the generic form
gr2(n,m:CURRENT): CURRENT
ensure
Result = if n=0 then c(m)
else h((m->gr2(n.pred,m)),n)(m)
end
end
-- where
gr(n.pred) = (m->gr2(n.pred,m))
If we match the definition of +
with the generic definition of gr2
we get
the following correspondences:
c = (m -> m)
h = ([g,n] -> m -> g(m).succ)
For the functions *
and ^
we get the following:
* (n,m:CURRENT): CURRENT
deferred
ensure
Result = if n=0 then 0
else (n.pred*m) + m
end
end
c = (m -> 0)
h = ([g,n] -> m -> g(m) + m)
^ (n,m:CURRENT): CURRENT -- '^' is recursive in the second argument!!
deferred
ensure
Result = if m=0 then 1
else
n^m.pred * n
end
end
c = (n -> 1)
h = ([g,m] -> n -> g(n) * n)
-- Note: because '^' is recursive in the second argument we get
-- gr2(m,n) = n^m !!
The general definition of a (primitive) recursive function on natural numbers
looks like:
gr(n:CURRENT): G
ensure
Result = if n=0 then c
else h(gr(n.pred),n)
end
end
with c:G
and h:[G,CURRENT]->G
where h
is a total function. From this
definition we extract the functional f
f(g:CURRENT->G): CURRENT->G
ensure
Result = (n -> if n=0 then c
else h(g(n.pred),n)
end)
end
so that the function gr
is a fixpoint of the functional f
.
gr = f(gr) -- gr is fixpoint of f
The functional f
depends only on c
and h
. Therefore we define a function
functional
which given c
and h
returns the functional.
functional(c:G, h:[G,CURRENT]->G): (CURRENT->G)->(CURRENT->G)
require
h.is_total
ensure
Result = (g -> n -> if n=0 then c
else h(g(n.pred),n)
end)
end
The functional f=functional(c,h)
transforms the domain of any function
g:CURRENT->G
according to the formula f(g).domain = {0} +
. I.e. zero is contained in
g.domain.image(succ)f(g).domain
as well as all
elements of g.domain
shifted one up. We prove this claim by the following
reasoning:
fdtrans:
all(c:G, h:[G,CURRENT]->G, f:(CURRENT->G)->(CURRENT->G), g:CURRENT->G)
require
r1: f = functional(c,h)
r2: h.is_total
check
c1: g.domain.image(succ) = {n: some(m) m.succ = n and m in g.domain}
-- def image
c2: g.domain.image(succ) = {n: n/=0 and n.pred in g.domain}
-- c1, def pred
c3: f(g).domain = {0} + {n: n/=0 and n.pred in g.domain}
-- r1, def functional
ensure
f(g).domain = {0} + g.domain.image(succ) -- c3,c2
end
Any functional f=functional(c,h)
is monotonic i.e. for all pairs of
functions a,b:CURRENT->G
with a<=b
we get f(a)<=f(b)
. Note that the
category of functions of type CURRENT->G
forms a complete partial order. The
relation a<=b
states that the domain of a
is a subset of the domain of
b
and that a
and b
restricted to the domain of a
(i.e. b|a.domain
)
are the same function.
We give a formal prove of the monotonicity of the functional f
.
fmono:
all(c:G, h:[G,CURRENT]->G, f:(CURRENT->G)->(CURRENT->G))
require
r1: h.is_total
r2: f = functional(c,h)
check
c1: all(a,b:CURRENT->G)
require
r1: a <= b
check
c2: a.domain <= b.domain -- r1
c3: {0} + a.domain.image(succ) <= {0} b.domain.image(succ)
-- c2
c4: f(a).domain <= f(b).domain -- c3,fdtrans
c5: all(n:CURRENT)
require
r2: n in f(a).domain
check
c6: n in f(b).domain
c7: n=0 or n/=0
c8: f(a)(0) = f(b)(0) -- = c
c9: require
r3: n/=0
check
c10: n.pred in a.domain -- r2
c11: n.pred in b.domain -- c10,c2
c12: a(n.pred)=b(n.pred) -- r1,c10,c11
c13: f(a)(n) = h(a(n.pred),n) -- def f
c14: f(b)(n) = h(b(n.pred),n) -- def f
ensure
f(a)(n) = f(b)(n) -- c12,c13,c14
end
ensure
f(a)(n) = f(b)(n) -- c7,c8,c9
end
c15: f(a) = f(b)|f(a).domain -- c4,c5
ensure
f(a) <= f(b) -- c4,c15
end
ensure
f.is_monotonic
end
We claim that the functional f=functional(c,h)
is continuous. Continuity
requires that the suprema of directed sets are maintained. I.e. if d
is a
directed set and s=d.supremum
its supremum then we have to prove that
d.image(f).supremum = f(s)
.
Since f
is monotonic it is guaranteed that d.image(f)
is a directed set
and that upper bounds of all sets are mapped to upper bounds. This implies
d.image(f) <= f(s)
.
In order to prove that f(s)
is the least upper bound of d.image(f)
we show
that the assumption of another upper bound g
with d.image(f)<=g
and
g<f(s)
leads to a contradiction.
If there exists such an upper bound g
then there has to be one element n
in the domain of f(s)
which is not in the domain of g
. In the case n=0
the contradiction is evident because all functions in d.image(f) have zero in
their domain and therefore g
cannot be an upper bound. In the case n/=0
we
get n.pred in s.domain
because n in f(s).domain
. This implies that there
has to be one function a
within d
so that n.pred in a.domain
otherwise
s
would not be a supremum. This implies that there is another function a in
so that
d.image(f)n in a.domain
. Therefore n
has to be in the domain of
g
because g
is an upper bound of d.image(f)
. This contradicts the fact
that n /in g.domain
.
fconti:
all(c:G, h:[G,CURRENT]->G, f:(CURRENT->G)->(CURRENT->G))
require
r1: h.is_total
r2: f = functional(c,h)
check
c1: all(d:(CURRENT->G)?, s:CURRENT->G)
require
r3: d.is_directed
r4: s = d.supremum
check
c2: d <= s -- r4
c3: d.image(f) <= f(s) -- c2, fmono
c4: require
r5: some(g:CURRENT->G)
d.image(f)<=g and g<f(s)
ensure
false -- contra_fconti (see below)
end
c5: all(g:CURRENT->G)
d.image(f)<=g => f(s)<=g
-- c4
ensure
d.image(f).supremum = f(s) -- c3,c5
end
ensure
f.is_continuous
end
contra_fconti:
all(c:G, h:[G,CURRENT]->G,
f:(CURRENT->G)->(CURRENT->G),
d: (CURRENT->G)?, s,g:CURRENT->G,
)
require
r1: h.is_total
r2: f = functional(c,h)
r3: d.is_directed
r4: s = d.supremum
r5: d.image(f) <= g
r6: g < f(s)
check
c1: some(n:CURRENT) n in (f(s).domain-g.domain) -- r6
c2: all(n:CURRENT)
require
r7: n in f(s).domain
r8: n /in g.domain
check
c3: all(a) a in d.image(f) => 0 in a.domain -- fdtrans
c4: 0 in g.domain -- c3,r5
c6: n=0 or n/=0
c7: n=0 => n in g.domain and n /in g.domain -- c4,r8
c8: require
r9: n/=0
check
c9: n.pred in s.domain -- r7,r9,fdtrans
c10: some(a:CURRENT->G)
a in d and n.pred in a.domain
-- c9,r4
c11: some(a:CURRENT->G)
a in d.image(f) and n in a.domain
-- c10,fdtrans
c12: n in g.domain -- c11,r5
ensure
false -- c12,r8
end
ensure
false -- c6,c7,c8
end
ensure
false -- c1,c2
end
We cite the central fixpoint law from the article “Closures and fixpoints”:
Whenever an endofunction f
on a complete partial order is continuous and
there is an element a
which is a prefixpoint of f
and the set
a.closed(f)
and its supremum are completely in the domain of f
then
a.closed(f).supremum
is the least fixpoint above a
(Note that
a.closed(f)
is a chain and is therefore directed so that the supremum
exists).
CPO: COMPLETE_PARTIAL_ORDER
least_fixpoint:
all(a,s:CPO, f:CPO->CPO)
require
f.is_continuous
a in f.prefixpoints
s = a.closed(f).supremum
a.closed(f) + {s} <= f.domain
ensure
s.is_least(f.fixpoints * {x:a<=x})
end
We can apply this theorem to the functions of type CURRENT->G
and the
functional f=functional(c,h)
.
Since the functions form a complete partial order and the completely undefined
function 0
is the least function within this order we get that 0
is a
prefixpoint of the functional f
and (0).closed(f).supremum
is the least
fixpoint of the functional f
.
From the least_fixpoint
theorem it follows that the supremum s
with
s=(0).closed(f).supremum
is the least fixpoint of a functional of the form
f=functional(c,h)
.
We claim that the supremum s
is a unique fixpoint of the functional
f
. We prove this claim by showing that s
is a total function. If s
is a
total function then there is no function above s
i.e. there cannot be any
fixpoint above s
(and there is no fixpoint below s
because s
is the
least fixpoint).
In order to demonstrate that s
is a total function we look at the functions
in the closure (0).closed(f)
. For each natural number n
there is an
element in the closure which has n
in its domain. We proof this by induction
on n
(see below). If this is the case then all natural numbers n
must be
in the domain of the supremum s
as well. Otherwise s
were not an upper
bound of (0).closed(f)
.
all(n:CURRENT,
c:G, h:[G,CURRENT]->G,
f:(CURRENT->CURRENT)->CURRENT->CURRENT)
require
r1: f = functional(c,h)
check
c1: f(0) in (0).closed(f)
c2: 0 in f(0).domain
c3: all(n:CURRENT)
require
r2: some(g) g in (0).closed(f) and n in g.domain
check
c4: all(g:CURRENT->G)
require
r3: g in (0).closed(f)
r4: n in g.domain
check
c5: f(g) in (0).closed(f) -- f is total
c6: f(g).domain
= {0}+g.domain.image(succ) -- fdtrans
c7: n.succ in f(g).domain -- r4,c6
ensure
some(g)
g in (0).closed(f)
and n.succ in g.domain -- c5,c7, witness f(g)
end
ensure
some(g)
g in (0).closed(f)
and n.succ in g.domain -- r2,c4
end
ensure
some(g:CURRENT->G)
g in (0).closed(f)
and n in g.domain -- c1,c2,c3, induction
end
Endofunctions are functions f of type A->A where A can be any type. Since all
functions are partial functions we can use endofunctions to model linked
structures. E.g. if we have an object which has an optional reference to an
object of the same type the optional reference can be viewed as a partial
function f of type A->A. If the reference to the next object is void (there is
no next object because we are at the end of a linked list or we have reached
the root of a tree following the parent links) then the corresponding function
f is undefined for this specific object.
If we have an element a of type A and an endofunction f of type A->A we can
start at the element a, apply the function to a (if a is in the domain of the
function) getting f(a), reapply the function to f(a) (provided that f(a) is in
the domain of f), … I.e. we get the set
{a, f(a), f(f(a)), ... }
We call a set p of elements of type A closed under f if the image of p under f
is a subset of p, i.e. f applied to any element of p results in an element
within p. I.e. we define the predicate
is_closed(p:A?, f:A->A): ghost BOOLEAN -- Is the set `p' closed under the function `f'? ensure Result = p.image(f) <= p end
and remember the definition of image as
image(p:A?, f:A->B): ghost B? -- The image of the set `p' under `f'. ensure Result = {b: some(a) a in p and a in f.domain and f(a) = b} end
With this we get an equivalent definition of a closed set as
all(p:A?, f:A->A) ensure p.is_closed(f) = all(x:A) require x in p x in f.domain ensure f(x) in p end end
The proof of the equivalence of both definitions is a standard exercise.
The set of all sets which are closed under an endofunction f form a closure
system. Therefore we can close any set p under a function f by finding the
least set which is closed under f and contains the set p.
closed(p:A?, f:A->A): ghost A? -- The set `p' closed under the function `f'. ensure Result = {q:A?: q.is_closed(f) and p<=q}.infimum end
Since the set {q: q.is_closed(f)} is a closure system the above used infimum
is guaranteed to be closed under f (for details on closures see the article
“Complete lattices and closure systems”). We often need the closure of the
singleton set {a} under f and therefore define the closure of an element a by
closed(a:A, f:A->A): ghost A? -- The singleton set {a} closed under the function `f'. ensure Result = {a}.closed(f) end
With this definition we can write the above set in a closed form
a.closed(f) = {a, f(a), f(f(a)), f(f(f(a))), ... }
The function ‘closed’ is a closure operator, i.e. it is monotonic, ascending
and idempotent on the first argument.
all(p,q:A?, f:A->A) ensure monotonic: p <= q => p.closed(f) <= q.closed(f) ascending: p <= p.closed(f) idempotent: p.closed(f) = p.closed(f).closed(f) end
Furthermore the function closed is monotonic in the function argument
all(a:A, p:A?, f,g:A->A) ensure f <= g => p.closed(f) <= p.closed(g) f <= g => a.closed(f) <= a.closed(g) end -- Note: f<=g iff f.domain <= g.domain and the values of f and g -- coincide on f.domain
Since p.closed(f) is the least set which contains all elements of p and is
closed under f all other sets which contain p and are closed under f must be
supersets of p.closed(f). We can use this property of the closure to prove
that all elements of a closure x satisfy a certain property e. In order to do
this we put all elements which satisfy e into the set q = {x: e}. If we can
prove that the initial set p is a subset of q and q is closed under f then we
can conclude that all p.closed(f) is a subset of q i.e. that all elements of
p.closed(f) satisfy the property e.
induction: all(p,q:A?, f:A->A) require p <= q q.is_closed(f) ensure p.closed(f) <= q end
This theorem is a direct consequence of the fact that the function closed is
monotonic, ascending and idempotent.
We can use a similar induction law if the start set is the singleton set {a}.
induction: all(a:A, q:A?, f:A->A) require a in q q.is_closed(f) ensure a.closed(f) <= q end
If we start from an element a and repeatedly apply f forever or until we reach
an out of domain element our intuition tells us that there are three
possibilities. Either we can continue reapplying f forever without reaching an
out of domain element and without reaching an element which has already been
passed or we reach some out of domain element or we reach an element which we
have already passed.
infinite chain: a -> b -> c -> .... finite chain: a -> b -> c -> .... -> z z /in f.domain with cycle: a -> b -> c -> .... -> z ^ / \........../
In order to deal with these structures we introduce the following definitions.
is_cycle(p:A?, f:A->A): ghost BOOLEAN -- Does the set `p' form a cycle under the function `f'? ensure Result = some(a) a.closed(f) = p and a in f.domain and a in f(a).closed(f) end has_cycle(p:A?, f:A->A): ghost BOOLEAN -- Does the set `p' contain a cycle under `f'? ensure Result = some(a) a in p and a.closed(f).is_cycle(f) end is_connected(p:A?, f:A->A): ghost BOOLEAN -- Is the set `p' connected under `f'? ensure Result = all(x,y:A) require {x,y} <= p ensure x in y.closed(f) or y in x.closed(f) end end is_chain(p:A?, f:A->A): ghost BOOLEAN -- Does the set `p' form a chain under `f'. ensure Result = (p.is_closed(f) and p.is_connected(f) and not p.has_cycle(f)) end
In any closure of the form p.closed(f) it is guaranteed that
all elements except the elements of the initial set have predecessors within
the closure.
In order to prove this claim we assume the opposite that for a certain element
x in p.closed(f)-p all elements a of p.closed(f) are not predecessors of
x. Since x has no predecessor we can prove that the set p.closed(f)-{x} is
closed under f and contains p. This set is strictly smaller than p.closed(f)
which contradicts the fact that p.closed(f) is the least set which contains p
and is closed under f.
some_predecessor: all(x:A, p:A?, f:A->A) require r1: x in p.closed(f)-p check c1: require r2: all(a) a in p.closed(f) => a in f.domain => f(a)/=x check c2: all(a:A) require r3: a in p.closed(f)-{x} r4: a in f.domain check c3: a in p.closed(f) -- r3 c4: a /= x -- r3 c5: f(a) /= x -- c3,r4,r2 c6: f(a) in p.closed(f) -- c3,r4 ensure f(a) in p.closed(f)-{x} -- c5,c6 end c7: p <= p.closed(f)-{x} -- r1 c8: (p.closed(f)-{x}).is_closed(f) -- c2 c9: p.closed(f) <= p.closed(f)-{x} -- closure is least c10: x in p.closed(f)-{x} -- r1,c9 c11: x /in p.closed(f)-{x} -- general law ensure False -- c10,c11 end ensure some(a) a in p.closed(f) and a in f.domain and f(a)=x end
By definition the set p is a cycle under f if there is one element a in the
domain of f such that p=a.closed(f) and a is in f(a).closed(f).
We claim that all elements of a cycle satisfy this property. We prove this
with a two step approach. First we prove the lemma that each element of a
cycle transfers this property to its successor f(a). Then we use induction to
prove that all elements of a.closed(f) and therefore all elements of p have
this property.
feature {NONE} lemma: all(a:A, p:A?, f:A->A) require r1: a.closed(f) = p r2: a in f.domain r3: a in f(a).closed(f) check c1: f(a).closed(f) <= a.closed(f) -- general property c2: a.closed(f) <= f(a).closed(f) -- r3, closure is monotonic c3: a = f(a) or a /= f(a) c4: require r4: a /= f(a) check c5: {f(a)} < f(a).closed(f) c6: a in (f(a).closed(f)-{f(a)}) -- r3,r4 c7: f(a).closed(f) - {f(a)} <= f(f(a)).closed(f) -- general property of closure ensure f(a) in f.domain -- c5 a in f(f(a)).closed(f) -- c6,c7 end ensure f(a).closed(f) = p -- r1,c1,c2 f(a) in f.domain -- c3,c4,r2 f(a) in f(f(a)).closed(f) -- c3,c4,r3 end
Having this lemma it is easy to prove that all elements of a cycle under f are
in the domain of f and their closure is the cycle and they can be reached by
their successor.
cycle_all: all(x:A, p:A?, f:A->A) require r1: p.is_cycle(f) r2: x in p local r3: q = {x:A: x.closed(f)=p and x in f.domain and x in f(x).closed(f)} check c1: some(a:A) a.closed(f)=p and a in f.domain and a in f(a).closed(f) -- r1 c2: all(a:A) require r4: a.closed(f) = p r5: a in f.domain r6: a in f(a).closed(f) check c3: a in q -- r3,r4,r5,r6 c4: q.is_closed(f) -- lemma c5: a.closed(f) <= q -- c3,c4 c6: p <= q -- c5,r4 ensure x.closed(f) = p -- r2,c6,r3 x in f.domain -- r2,c6,r3 x in f(x).closed(f) -- r2,c6,r3 end ensure x.closed(f) = p -- c1,c2 x in f.domain -- c1,c2 x in f(x).closed(f) -- c1,c2 end
If f is a function and b is an element of f’s domain then f-b is the function
f without b in its domain. We can subtract a set p from the domain of
f. Furthermore we can restrict the domain of a function. In order to express
these domain restrictions we use the following definitions.
- (f:A->B, p:A?): ghost A->B -- The function `f' without the elements of the set `p' in -- its domain. ensure Result.domain = f.domain - p Result <= f end - (f:A->B, a:A): ghost A->B -- The function `f' without the element `a' in its domain. ensure Result = f - {a} end | (f:A->B, p:A?): ghost A->B -- The function `f' with its domain restricted to `f.domain*p'. ensure Result.domain = f.domain * p Result <= f end
If a closure a.closed(f) has an element which is not in the domain of f then
this element is unique.
out_of_domain_unique: all(a,b,c:A, f:A->A) require {b,c} <= a.closed(f) {b,c} * f.domain = 0 ensure b = c -- lemma below end
Proof: Assume that b and c are different, i.e. there are two out of domain
elements in the closure. We put all objects whose closure contains b and c
into the set p. Clearly a is in the set p. Furthermore the set p is closed
under f. Assume that x is an element of p and in the domain of f. Thus x is
neither b nor c. Therefore f(x).closed(f) must contain b and c as well and is
therefore an element of p. So we have that all elements of a.closed(f) are in
p. This contradicts the fact that b and c are in a.closed(f) which cannot be
in p.
feature {NONE} -- Proof of 'out_of_domain_unique' lemma: all(a,b,c:A, f:A->A) require r1: {b,c} <= a.closed(f) r2: {b,c} * f.domain = 0 r3: b /= c local r4: p = {x:A: {b,c} <= x.closed(f)} check c1: a in p -- r4,r1 c2: all(x:A) require r5: x in p r6: x in f.domain check c3: x /= b -- r6,r2 c4: x /= c -- r6,r2 c5: {b,c} <= f(x).closed(f) -- r5,r4,c3,c4 ensure f(x) in p -- c5 end c6: p.is_closed(f) -- c2 c7: a.closed(f) <= p -- c1,c6 c8: b /in p -- r4,r2,r3 c9: b /in a.closed(f) -- c8,c7 c10: b in a.closed(f) -- r1 ensure False -- c9,c10 end end
If we have a closure of the form a.closed(f) and two elements b and c within
this closure then starting from a we either first encounter b or c. This
intuitively clear fact can be expressed by the claim
sequence: all(a,b,c:A, f:A->A) require {b,c} <= a.closed(f) ensure b in a.closed(f-c) or c in a.closed(f-b) end
We prove this claim by a series of intermediate lemmas.
First we show that all b in the closure a.closed(f) are also in the closure of
a.closed(f-b) (f-b is the function f with b removed from its domain). We prove
this claim by contradiction.
sequence_1: all(a,b:A, f:A->A) require r1: b in a.closed(f) check c1: require r2: b /in a.closed(f-b) check c2: b in f.domain or b /in f.domain c3: require r3: b in f.domain check c4: a.closed(f-b) = a.closed(f-b+{b->f(b)}) -- r2,r3, -- modification of a function outside -- the closure has no effect on the -- closure c5: a.closed(f-b) = a.closed(f) -- c4 c6: b /in a.closed(f) -- r2,c5 ensure False -- r1,c6 end c7: require r4: b /in f.domain check c8: f-b = b c9: b /in a.closed(f) -- r2,c8 ensure False -- r1,c9 end ensure False -- c2,c3,c7 end ensure b in a.closed(f-b) -- c1 end
Next we show that for any c in a.closed(f) and b in a.closed(f-c) that the
closure of a.closed(f-b) is a subset of the closure a.closed(f-c).
sequence_2: all(a,b,c:A, f:A->A) require r1: c in a.closed(f) r2: b in a.closed(f-c) check c1: b=c or b/=c -- first trivial c2: c in f.domain or c /in f.domain -- second trivial c3: require r3: c in f.domain r4: b /= c check c4: b in a.closed(f-c-b) -- r2, sequence_1 c5: c /in a.closed(f-c-b) -- c4, out_of_domain_unique c6: f-b = f-c-b + {c->f(c)} -- r3, general law c7: a.closed(f-c-b) = a.closed(f-b) -- c5,c6,modify_out_of_closure c8: a.closed(f-c-b) <= a.closed(f-c) -- monotonic ensure a.closed(f-b) <= a.closed(f-c) -- c7,c8 end ensure a.closed(f-b) <= a.closed(f-c) -- c1,c2,c3 end
Consider an element b and an element c which is not in the closure
a.closed(f-b). From this we can conclude that a.closed(f-b) is a subset of
a.closed(f-c).
sequence_3: all(a,b,c:A, f:A->A) require r1: c /in a.closed(f-b) check c1: a.closed(f-b) = a.closed(f-b-c) -- r1 c2: a.closed(f-b) = a.closed(f-c-b) -- c1 c3: a.closed(f-c-b) <= a.closed(f-c) -- monotonic ensure a.closed(f-b) <= a.closed(f-c) -- c2,c3 end
From these intermediate lemmas it is easy to prove the main theorem.
sequence_4: all(a,b,c:A, f:A->A) require r1: {b,c} <= a.closed(f) check c1: require r2: c /in a.closed(f-b) check c2: a.closed(f-b) <= a.closed(f-c) -- r1, r2, sequence_3 c3: b in a.closed(f-b) -- r1, sequence_1 ensure b in a.closed(f-c) -- c3,c2 end ensure b in a.closed(f-c) or c in a.closed(f-b) -- c1 end
In this chapter we concentrate on closures which have cycles or which are a
cycle.
If we have two sets p and q which form a cycle under f then they are either
identical or disjoint.
cycle_disjoint_or_identical: all(p,q:A?, f:A->A) require r1: p.is_cycle(f) r2: q.is_cycle(f) check c1: require r3: p*q /= 0 check c2: some(a:A) a in p and a in q -- r3 c3: all(a:A) require r4: a in p r5: a in q check c4: p = a.closed(f) -- r4,r1,cycle_all c5: q = a.closed(f) -- r5,r2,cycle_all ensure p = q -- c4,c5 end ensure p = q -- c2,c3 end ensure p*q=0 or p=q -- c1 end
A closure of the form a.closed(f) cannot have two different cycles.
cycle_unique: all(a:A, p,q:A?, f:A->A) require r1: p.is_cycle(f) r2: q.is_cycle(f) r3: p <= a.closed(f) r4: q <= a.closed(f) check c1: require r5: p /= q local r6: s = {a:A: not a.closed(f).is_cycle(f) and p+q<=a.closed(f)} check c2: p*q = 0 -- r1,r2,r5,cycle_disjoint_or_identical c3: not a.closed(f).is_cycle(f) -- r1,r2,r5,r3,r4 if a.closed(f) were a -- cycle it would have to be identical to -- p and q which contradicts r5 c4: a in s -- c3,r3,r4 c5: all(x:A) require r7: x in s r8: x in f.domain check c6: x /in p -- r7,r6,r1 c7: x /in q -- r7,r6,r2 c8: p+q <= f(x).closed(f) -- c6,c7 c9: not f(x).closed(f).is_cycle(f) -- if it were a cycle it would have to be -- identical to p and q which contradicts r5 ensure f(x) in s -- c8,c9 end c10: s.is_closed(f) -- c5 c11: a.closed(f) <= s -- c4,c10 c12: not p.is_cycle(f) -- c11,r3 ensure False -- r1,c12 end ensure p = q -- c1 end
In some previous chapter we have already proved that each element in
a.closed(f) except a is guaranteed to have a predecessor. In a cycle each
element is guaranteed to have a predecessor.
cycle_all_predecessor: all(p:A?, f:A->A) require r1: p.is_cycle(f) check c1: all(y:A) require r2: y in p check c1: y in f.domain -- r1,r2 c2: y in f(y).closed(f) -- r1,r2,cycle_all c3: f(y).closed(f)=p -- r1,r2,cycle_all c4: y=f(y) or y/=f(y) c5: y=f(y) => some(x) x in p and f(x)=y -- witness y c6: y/=f(y) => some(x) x in p and f(x)=y -- c2,c3,some_predecessor ensure some(x:A) x in p and f(x)=y -- c4,c5,c6 end ensure all(y) y in p => some(x) x in p and f(x)=y -- c1 end
If the set p is a cycle under f we claim that each element in p has a unique
predecessor. In order to prove this claim we assume that a, b and c are three
elements of the cycle where b and c are different predecessors of a and lead
this to a contradiction. As a first step we assume additionally that b is in
a.closed(f-c).
Since a, b and c are elements of the cycle they are in the domain of f. b and
c are different therefore b is in the domain of f-c, i.e (f-c)(b)=a because b
is a predecessor of a. Together with the additional assumption that b is in
a.closed(f-c) we get that b is in (f-c)(b).closed(f-c) which is sufficient to
conclude that b.closed(f-c) is a cycle and therefore that a.closed(f-c) is a
cycle as well. From the lemma ‘sequence_1 we can conclude that c is in
a.closed(f-c). In a cycle all elements are in the domain of the function,
therefore c must be in the domain of f-c which contradicts the definition of
f-c.
feature {NONE} lemma: all(a,b,c:A, f:A->A) require r1: a.closed(f).is_cycle(f) r2: {b,c} <= a.closed(f) r3: f(b) = a r4: f(c) = a r5: b in a.closed(f-c) check c1: require r7: b /= c check c2: b in (f-c).domain -- r1,r2,r7 c3: (f-c)(b)=a -- c2,r3 c4: b in (f-c)(b).closed(f-c) -- c3,r5 c5: b.closed(f-c).is_cycle(f-c) -- c4 c6: a.closed(f-c).is_cycle(f-c) -- c5,c3 c7: c in a.closed(f-c) -- r2,sequence_1 c8: c in (f-c).domain -- c6,c7,cycle_all c9: c /in (f-c).domain -- by definition ensure False -- c8,c9 end ensure b = c end end
Having this lemma we can prove that every element of a cycle has a unique
predecessor.
cycle_unique_predecessor: all(p:A?, f:A->A) require r1: p.is_cycle(f) check c1: all(a,b,c:A) require r2: {a,b,c} <= p r3: f(b) = a r4: f(c) = a check c2: a.closed(f) = p -- r1,r2,cycle_all c3: {b,c} <= a.closed(f) -- c2,r2 c4: b in a.closed(f-c) or c in a.closed(f-b) -- c3,sequence ensure b = c -- c4,r2,r3,r4,lemma end ensure all(a,b,c) {a,b,c} <= p => f(b)=a => f(c)=a => b=c end
As a corollary we get the fact that if the set p is a cycle under f then f is
injective on p.
cycle_injective: all(p:A?, f:A->A) require r1: p.is_cycle(f) check c1: all(b,c:A) require r2: {b,c} in (f|p).domain r3: f(b) = f(c) check c2: p <= f.domain -- r1 c3: (f|p).domain = p -- c2 c4: {b,c} <= p -- r2,c3 ensure b = c -- r2,r3,c4, -- cycle_unique_predecessor end ensure (f|p).is_injective end -- Note: f|p is the function f restricted to the domain -- f.domain*p
If a closure a.closed(f) has a cycle then it is guaranteed that all elements
of the closure are within the domain of f. This claim is easy to prove. We put
all elements of f.domain whose closure has a cycle into the set p and show that
a is in p and that p is closed under f.
The element a is in p because a.closed(f) has a cycle. Therefore either
a is in the cycle and is therefore in f.domain or it is not in the cycle. If
it is not in the cycle and not in the domain then a.closed(f) could not
contain a cycle.
Assume an arbitrary element x of p i.e. x is in f.domain and x.closed(f) has a
cycle. Since x.closed(f) has a cycle f(x).closed(f) has a cycle as well. Now
either f(x).closed(f) is a cycle. Then f(x) is in p as well. Or f(x).closed(f)
is not a cycle. Then f(x) must be in f.domain otherwise it could not contain a
cycle.
cycles_in_domain: all(a:A, f:A->A) require r1: a.closed(f).has_cycle(f) local r2: p = f.domain * {x:A: x.closed(f).has_cycle(f)} check c1: a in f.domain -- r1 c2: a in p -- c1,r2 c3: all(x:A) require r3: x in p r4: x in f.domain check c4: x.closed(f).has_cycle(f) -- r3,r2 c5: f(x).closed(f).has_cycle(f) -- c4 c6: f(x).closed(f).is_cycle(f) or not f(x).closed(f).is_cycle(f) c7: require r5: f(x).closed(f).is_cycle(f) check c8: f(x) in f.domain ensure f(x) in p -- c8,c5 end c9: require r6: not f(x).closed(f).is_cycle(f) check c10: f(x) in f.domain -- r6,c5 ensure f(x) in p -- c10,c5 end ensure f(x) in p end c11: p.is_closed(f) -- c3 c12: a.closed(f) <= p -- c1,c11 ensure a.closed(f) <= f.domain -- c12,r2 end
If we have a closure of the form a.closed(f) we can take any element b of the
closure and form the subclosure b.closed(f). From the properties that ‘closed’
is monotonic, ascending and idempotent it is immediately evident that
b.closed(f) is a subset of a.closed(f).
But there are more properties in subclosures. Suppose we pick two elements b
and c of a.closed(f). Then we claim that either b is in the subclosure
c.closed(f) or c is in the subclosure b.closed(f) i.e. we can reach either b
from c or c from b which says that all elements of the closure are connected.
We are going to prove this claim by induction. But before doing the induction
let us split out the induction step.
Assume that b and c are in a.closed(f) and c is in b.closed(f). Then we prove
that either c is in f(b).closed(f) or f(b) is in c.closed(f).
feature {NONE} lemma: all(a,b,c:A, f:A->A) require r1: {b,c} <= a.closed(f) r2: c in b.closed(f) r3: b in f.domain check c1: b=c or b/=c c2: require r4: b=c check c3: f(b) in c.closed(f) -- r3,r4 ensure c in f(b).closed(f) or f(b) in c.closed(f) -- c3 end c4: require r5: b/=c check c5: c in f(b).closed(f) -- r2,r3,r5 ensure c in f(b).closed(f) or f(b) in c.closed(f) -- c5 end ensure c in f(b).closed(f) or f(b) in c.closed(f) -- c1,c2,c4 end end
Having this lemma we can prove the main theorem.
subclosure: all(a,b,c:A, f:A->A) require r1: {b,c} <= a.closed(f) local r2: p = {b:A: c in b.closed(f) or b in c.closed(f)}*a.closed(f) check c1: a in p -- r2,r1 c2: all(x:A) require r3: x in p r4: x in f.domain check c3: x in a.closed(f) c4: c in x.closed(f) or x in c.closed(f) c5: require r5: c in x.closed(f) check c6: c in f(x).closed(f) or f(x) in c.closed(f) -- r4,r1,c3,r5,lemma ensure f(x) in p -- c3,r2,c6 end c7: require r6: x in c.closed(f) check c8: f(x) in c.closed(f) -- r6,r4 ensure f(x) in p -- c8,c3 end ensure f(x) in p -- c4,c5,c7 end c9: p.is_closed(f) -- c2 c10: a.closed(f) <= p -- c1,c9 ensure c in b.closed(f) or b in c.closed(f) -- c10,r2 end
Whenever we have a set p which is a chain under the function f we can conclude
that f is injective on p.
Remember that a function g is injective if for all pairs x and y in the domain
of g the equality g(x)=g(y) implies x=y. The function g is our function f
restricted to p i.e. f|p. We claim that f|p is injective if p is a chain under
f.
We prove this claim by contradiction. Assume that f|p is not injective. Then
there are x and y in the domain of f|p such that f(x)=f(y) and x/=y. Without
loss of generality we can assume that y is in x.closed(f) (see definition of a
chain).
Since y is in x.closed(f) and x/=y we get y in f(x).closed(f) and therefore y
in f(y).closed(f). This implies that y.closed(f) is a cycle which contradicts
the fact that p is a chain.
In order to keep the proof readable we split out the contradiction as a lemma
and then prove the main theorem.
feature {NONE} lemma: all(p:A?, x,y:A, f:A->A) require r1: p.is_chain(f) r2: {x,y} <= (f|p).domain r3: f(x) = f(y) r4: x /= y r5: y in x.closed(f) check c1: y in f(x).closed(f) -- r5,r4 c2: y in f(y).closed(f) -- c1,r3 c3: y.closed(f).is_cycle(f) -- r2,c2 c4: not y.closed(f).is_cycle(f) -- r2,r1 ensure False -- c3,c4 end end
chain_injective: all(p:A?, f:A->A) require r1: p.is_chain(f) check c1: require r2: not (f|p).is_injective check c2: some(x,y:A) {x,y}<=(f|p).domain and f(x)=f(y) and x/=y -- r2 c3: all(x,y:A) require r3: {x,y} <= (f|p).domain r4: f(x) = f(y) r5: x /= y check c4: y in x.closed(f) or x in y.closed(f) -- r1,r3 c5: y in x.closed(f) => False -- r1,r3,r4,r5,lemma c6: x in y.closed(f) => False -- r1,r3,r4,r5,lemma ensure False -- c4,c5,c6 end ensure False -- c2,c3 end ensure (f|p).is_injective end
Consider a closure a.closed(f) which is a chain under f. The image of the
closure under f does not contain a.
all(a:A, f:A->A) require r1: a.closed(f).is_chain(f) check c1: a in f.domain or a /in f.domain c2: require r2: a in f.domain check c3: a.closed(f) = {a} + f(a).closed(f) -- general law c4: a /in f(a).closed(f) -- r1 c5: f(a).closed(f) = a.closed(f) - {a} -- c3,c4 c6: a.closed(f).image(f) = f(a).closed(f) -- general law ensure a.closed(f).image(f) = a.closed(f) - {a} -- c5,c6 end c7: require r3: a /in f.domain check c8: a.closed(f) = {a} -- r3 c9: {a}.image(f) = 0 -- r3 ensure a.closed(f).image(f) = a.closed(f) - {a} -- c8,c9 end ensure a.closed(f).image(f) = a.closed(f) - {a} -- c1,c2,c7 end
In this chapter we want to prove the following facts:
– Each chain which has no out of domain elements is infinite.
– Each chain which has an out of domain element is finite.
– Each cycle is finite.
– Each closure which has a cycle is finite.
In this chapter we don’t do all proofs in full formality. Some proofs will be
given with textual arguments only. However it is an easy exercise to
construct the formal proofs.
Let us recall the definition of infiniteness of a set.
is_infinite(p:A?): ghost BOOLEAN -- Is the set `p' infinite? ensure Result = some(f:A->A) f.is_injective and p <= f.domain and p.image(f) < p end
I.e. a set p is infinite if there is an injective map f mapping all elements
of p to a proper subset of p.
A set is finite if it is not infinite.
is_finite(p:A?): ghost BOOLEAN -- Is the set `p' finite? ensure Result = not p.is_infinite end
This gives us an equivalent definition of finiteness.
all(p:A?) ensure p.is_finite = all(f:A->A) require f.is_injective p <= f.domain p.is_closed(f) -- i.e. p.image(f)<=p ensure p = p.image(f) end end
From the definitions we can immediately conclude that the empty set is
finite since there is no proper subset of the empty set (and therefore not any
function mapping to a proper subset).
Furthermore we can prove that p+{a} is a finite set provided that p is a
finite set. Without loss of generality we can analyze the case that the
element a is not in p (if a is in p then p+{a}=p which is a trivial case).
Let’s assume the opposite that p+{a} is infinite. Then there is a injective
function f which maps p+{a} to a proper subset. We analyze the case where a is
in the image and where a is not in the image.
If a is not in the image then (p+{a}).image(f) <= p. This implies p.image(f)<p
because f(a) is missing in the image. Thus p must be infinite which
contradicts the fact that p is finite.
If a is in the image then there is a unique element b in p+{a} with f(b)=a
because of the injectivity of f. We define a new function g which is f with
the values for a and b swapped. g is still injective with g(a)=a and
(p+{a}).image(g)<p+{a}. Thus p.image(g)<p which says that p is infinite and
therefore contradicts the assumption that p is a finite set.
all(a:A, p:A?) require r1: (p+{a}).is_infinite r2: a /in p check c1: all(f:A->A) require r3: f.is_injective r4: p+{a} <= f.domain r5: (p+{a}).image(f) < p+{a} check c2: a /in (p+{a}).image(f) or a in (p+{a}).image(f) c3: require r6: a /in (p+{a}).image(f) check c4: (p+{a}).image(f) <= p -- r5,r6 c5: p.image(f) = (p+{a}).image(f)-{f(a)} -- r2 c6: p.image(f) < p -- c4,c5 ensure p.is_infinite -- r3,r4,c6 end c7: require r7: a in (p+{a}).image(f) local r8: b = (-f)(a) -- -f is the inverse of f r9: g = f <+ {a->f(b),b->f(a)} check c8: g.is_injective -- r3,r9 c9: g(a) = a -- r8,r9 c10: (p+{a}).image(g) < p+{a} -- r5,r9 c11: p.image(g) < p -- c9,c10 c12: p <= g.domain -- r4,r9 ensure p.is_infinite -- c8,c11,c12 end ensure p.is_infinite -- c2,c3,c7 end ensure p.is_infinite -- r1,c1 end
all(a:A, p:A?) require r1: p.is_finite check c1: a in p or a /in p c2: a in p => (p+{a}).is_finite -- trivial c3: require r2: a /in p r3: (p+{a}).is_infinite check c4: p.is_infinite -- r2,r3,previous theorem ensure False -- r1,c4 end ensure (p+{a}).is_finite -- c1,c2,c3 end
Let a.closed(f) be a chain under the function f and all elements of the chain
are in the domain of f. This implies that the set p is infinite.
Proof: Since f is an injection on a.closed(f) and a.closed(f) is completely
contained in the domain of f and f maps the chain to a proper subset
(a.closed(f).image(f) does not contain a) we have a witness for the injective
function required in the definition of infiniteness.
all(a:A, f:A->A) require r1: a.closed(f).is_chain(f) r2: a.closed(f) <= f.domain check c1: (f|a.closed(f)).is_injective -- r1 c2: a.closed(f) <= (f|a.closed(f)).domain -- r2 c3: a.closed(f).image(f|a.closed(f)) = a.closed(f).image(f) -- general law c4: a.closed(f).image(f) = a.closed(f)-{a} c5: a.closed(f).image(f) < a.closed(f) -- c4 c6: a.closed(f).image(f|a.closed(f) < a.closed(f) -- c3,c5 c7: some(g:A->A) g.is_injective and a.closed(f) <= g.domain and a.closed(f).image(g) < a.closed(f) -- c1,c2,c6, witness f|a.closed(f) ensure a.closed(f).is_infinite -- c7 end
This theorem implies in the contrapositive that any finite chain of the form
a.closed(f) must have one out of domain element.
all(a:A, f:A->A) require a.closed(f).is_chain(f) a.closed(f).is_finite ensure some(x:A) x in a.closed(f) and x /in f.domain -- contrapositive of the above theorem end
In the previous chapter we have proved that all finite chains of the form
a.closed(f) must have some out of domain element. In the following we prove
that the existence of an out of domain element in a chain of the form
a.closed(f) is sufficient to conclude that the chain is finite.
We first prove that for all elements x in a.closed(f) the closure
a.closed(f-x) is finite. We do this by induction. We put all elements x with
a.closed(f-x).is_finite into a set p. The start element a is in this set
because a.closed(f-a) is the one element set {a} which is definitely
finite. Now assume that a.closed(f-x) is finite and x is in the domain of
f. The set a.closed(f-f(x)) is equal to the set a.closed(f-x) + {f(x)}. Since
a.closed(f-x) is finite a.closed(f-f(x)) is finite as well and therefore p is
closed under f. Thus for all x in a.closed(f) we have a.closed(f-x) is finite.
Now assume that there is one element e in a.closed(f) which is not in the
domain of f. Therefore f = f-e. Since e is in a.closed(f) we have
a.closed(f-e) is finite and therefore a.closed(f) is finite as well.
Let’s assume that a.closed(f) is a cycle. From this we conclude that a is in
f(a).closed(f) and f(a).closed(f) = a.closed(f) by definition of a cycle. By
induction we can prove that all elements of f(a).closed(f) are in
f(a).closed(f-a). Since f-a <= f and the closure is monotonic in the function
we get the equality f(a).closed(f) = f(a).closed(f-a). Since f(a).closed(f-a)
is a chain with an out of domain element it is finite. Therefore a.closed(f)
is finite as well.
Let’s assume that a.closed(f) has a cycle. Then we claim that a.closed(f) is
finite.
From our intuition we know that there is an element e in a.closed(f) so that
a.closed(f-e) is a chain which contains all elements of a.closed(f). Since a
chain with one out of domain element is finite we can conclude that
a.closed(f) is finite. It remains to be proved that an element e exists within
a.closed(f) such that a.closed(f) = a.closed(f-e).
We prove this claim by contradiction. We assume that for all elements e of
a.closed(f) we have a.closed(f-e) < a.closed(f). We define a set p = {x:
all(e) e in x.closed(f) => x.closed(f-e)<x.closed(f)}. From our assumption we
conclude that a is in this set. Furthermore we can prove that p is closed
under f. Assume an element x in p and in the domain of f. x.closed(f) does not
contain an element e so that x.closed(f-e)=x.closed(f). Since f(x).closed(f)
is a subset of x.closed(f) the same applies to f(x). Therefore f(x) is in p as
well.
But a.closed(f) has a cycle. I.e. there is an element b in a.closed(f) so that
b.closed(f) is a cycle. f is injective on b.closed(f) therefore there is a
unique predecessor e of b which is also in a.closed(f). By the same reasoning
as in the previous chapter we can conclude that b.closed(f-e)=b.closed(f) which
contradicts the assumption that there is no element a.closed(f) which has this
property. Therefore the assumption must be wrong that there is no element e in
a.closed(f) so that a.closed(f-e)=a.closed(f).
Nearly all languages with imperative elements have some kind of an array. In C
an array is just a typed pointer to a memory region. You can address the
different elements by giving an offset to the start of the memory region. The
compiler multiplies the offset with the size of the objects contained in the
array to get the actual offset in bytes.
The C view of arrays has certain advantages: It is memory efficient because it
uses just one pointer. Accessing elements is easy because it just needs some
simple address arithmetic.
But the C view has some pitfalls as well. First you might fail to allocate
memory for the array. Then the pointer to the memory region has some undefined
value. Second you might read elements of the array which have not yet been
initialized. Third you might read from or write to elements of the array which
are not within the allocated region.
Modern languages avoid this pitfalls by certain methods which can be executed
at compile time or at runtime. The failure to initialize the pointer to the
memory region is usually handled by initializing all pointers with zero. Any
access to a zero pointer results in a runtime exception. Addressing elements
outside the region is usually handled at runtime as well by storing with the
array its capacity and generating an exception if elements outside the region
are addressed.
All these solutions avoid memory corruption but they have a cost. Additional
code is required to check null pointer accesses and to check out of bound
accesses. The single pointer implementation is no longer sufficient because
the size of the allocated region has to be stored in the runtime object.
A language which allows formal verification can avoid all these pitfalls
without any cost. Without any cost? Well — without any runtime and memory
footprint cost. But nothing is free. Some assertions must be provable at
compile time. I.e. whenever you want to access some element of the array you
have to be sure that the index is within the array bounds. And it is not
sufficient that you are sure. You have to convince the verifier of this fact.
In the following article we show an array structure in our programming
language which allows us to convince the verifier that we make no illegal
accesses.
Since an array is a mutable structure we have to address the framing problem
as well. The framing problem is addressed appropriately if for any modifying
procedure it is clear not only what it does change but also what it leaves
unchanged.
A lot of effort is done currently in the verification community to address the
framing problem. Frame specifications like modify and use clauses for each
procedure, separation logic, different sophisticated heap models etc.
In this article we demonstrate that the framing problem can be solved without
all these complicated structures. It is sufficient to regard access to data
structures as functions and have a sufficient powerful function model. It is
amazing to see that proper abstraction makes some difficult seeming problems a
lot easier.
As a warm up let us see what kind of operations we expect from arrays. First
of all we want to be able to declare variables of type array which hold
elements of a certain type.
local s: STRING a: ARRAY[STRING] do ...
The declaration does not allocate the object. Therefore accessing any element
of the array has to be illegal at this point.
a[4] := "Hello" -- Illegal, array not yet allocated!
To allocate the object we need some kind of object creation.
create a.with_capacity(10)
This instruction creates an array object with the possibility to store up to
10 strings. The creation just allocates the structure but does not populate
the array with any string.
At this point it is illegal to read elements.
s := a[0] -- Illegal, access to an uninitialized object!
But we can write some elements of the array and access these elements
a[0] := "first string" a[1] := "second string" s := a[1] s := a[2] -- Illegal, access to an uninitialized object! s := a[100] -- Illegal, out of bound access! ... end
If the verifying compiler is able to detect these illegal accesses and let
pass only the legal accesses, then the actual runtime code can have the same
size as the corresponding C code and the actual array object can be
represented by a single pointer to the memory region.
The type ARRAY is declared within the module array
-- module: array
We need a formal generic which represents the type of the elements.
G: ANY -- Element type
There are no constraints on the element type. Any type can be used as element
type.
The generic class ARRAY has three basic access functions and an invariant.
class ARRAY[G] feature capacity: ghost NATURAL -- The allocated capacity of the array. domain: ghost NATURAL? -- The set of indices of elements which have been initialized. [] (i:NATURAL): G -- Element at position `i'. require i in domain note built_in end invariant domain <= {i: i<capacity} -- Note: '<=' for sets is the subset relation end
‘capacity’ returns the allocated capacity of the array. Since it is a ghost
attribute it can be used only in assertions and cannot flow into actual
computations. The attribute ‘domain’ represents the set of indices of array
elements which have already been initialized. This is a ghost attribute as
well.
The bracket operator ([]) represents the function which returns the element
at position ‘i’. It uses ‘domain’ in its precondition to express that only
elements at initialized positions can be allocated.
The invariant states that only elements within the allocated capacity can be
initialized.
We have presented the above declaration of the class ARRAY in an object
oriented manner. Programmers familiar with object oriented languages like
C++, Java, C# should have no problems to read such a declaration. But it
should be noted that such a form of declaration hides one important thing. All
routines declared within the class ARRAY have one implicit argument which is
not explicitly mentioned, namely the array object (called ‘this’ in C++ and
Java). In order to make this argument more explicit we present this
declaration in a different but equivalent form.
class ARRAY[G] end feature capacity(a:CURRENT): ghost NATURAL domain(a:CURRENT): ghost NATURAL? [](a:CURRENT, i:NATURAL): G require i in a.domain note built_in end end invariant all(a:CURRENT) ensure a.domain <= {i: i < a.capacity} end end
This form of the declaration makes transparent that the used identifiers and
operators have the following types:
capacity: ghost CURRENT->NATURAL domain: ghost CURRENT->NATURAL? ([]): [CURRENT,NATURAL] -> G
Remark: In our programming language we use the Haskell convention that an
operator (unary or binary) put between parentheses can be used like an
ordinary function. I.e. for numbers the expressions ‘7+3’ and ‘(+)(7,3)’ are
equivalent, but the first one is preferable because it is much more readable.
If something looks like an attribute in object oriented notation it is
actually a function because it needs an argument. Therefore an assignment to
an attribute is something fundamentally different from the assignment to a
local variable. An assignment of a new value to an attribute is a modification
of the corresponding function.
Note that the unfolded form of the class invariant makes evident that the
invariant is not only an object invariant but an invariant for the whole
system. In the case of such simple classes as ARRAY this makes no
difference. But if we look at linked structures the difference is fundamental.
The function domain has been used in the precondition of the function
([]). This creates the following implicit invariant.
invariant ([]).domain = {a:CURRENT, i:NATURAL: i in a.domain} end
Since procedures might modify the functions ([]) and domain, they have to
respect the invariant, the explicit and the implicit invariant.
The basic access functions capacity, domain and ([]) are independent in the
sense that the value of one function does not depend on the value of any other
or these basic functions. There is a consistency condition between domain and
([]) because the function domain is used in the precondition of ([]) (see
previous chapter). However the value returned by ([]) does not depend on the
function domain.
The basic access functions capacity and domain have another consistency
condition expressed in the invariant. Note that consistency conditions do not
establish a dependence.
Based on the basic access functions we can define other functions. E.g. we
might define a function is_full which returns true if all positions in the
array are accessible.
is_full(a:CURRENT): ghost -- Are all positions of the array `a' accessible? ensure Result = ({i: i < a.capacity} = a.domain) end
This function has a clear dependency: It depends on the functions capacity and
domain. This implies that any modification of the functions capacity and/or
domain modifies implicitly the function is_full as well.
In order to be able to create an object of type array we need a creation
procedure (or a constructor called in languages like C++, Java and C#).
class ARRAY[G] create with_capacity(c:NATURAL) -- Create an array with capacity `c'. note built_in ensure domain = 0 -- '0' is the empty set capacity = c end end
In order to make the hidden argument more transparent we take the declaration
out of the class and get the following equivalent declaration.
create with_capacity(a:CURRENT, c:NATURAL) note built_in ensure a.domain = 0 a.capacity = c end end
Each creation procedure receives as its first argument an allocated but
uninitialized object of the corresponding type. The task of the creation
procedure is to initialize the object such that it satisfies the consistency
conditions expressed in the invariant.
The postcondition states that the creation procedure has an effect on the
functions ‘domain’ and ‘capacity’. Remember that the corresponding types are
CURRENT->NATURAL? and CURRENT->NATURAL.
Expressions like ‘a.domain=0’ in postconditions of procedures are interpreted
in our language rather strictly. It reads: the identifier ‘domain’ identifies
a new function which is the same as the function which it identified before
the call except for the argument ‘a’. Only functions mentioned in the
postcondition (and functions which depend on them) are modified by a
procedure. I.e. the declaration of the creation procedure is interpreted in the
following manner.
create with_capacity(a:CURRENT, c:NATURAL) note built_in ensure domain = old (domain + [a,0]) capacity = old (capacity + [a,c]) ([]) = old ([]) end end
For any function f:A->B the expression ‘f+[a,b]’ is defined as
f + [a,b] = (x -> if x=a then b else f(x) end)
with the consequences
(f+[a,b]).domain = f.domain + {a} x=a => (f+[a,b])(x) = b x/=a => (f+[a,b])(x) = f(x)
I.e. we view functions as sets of key value pairs then the expression ‘f+[a,b]’
is the set of key value pairs of the function ‘f’ except for the key ‘a’ where
the pair [a,b] either overrides the one already contained in ‘f’ or is added
to the set of key value pairs.
The strict interpretation of the postcondition of ‘with_capacity’ corresponds
to our intuition if we read expressions like ‘a.domain=0’ in the postcondition
of a procedure. Our intuition tells us the ‘a.domain’ has been set to ‘0’ but
‘b.domain’, ‘c.domain’, … for ‘b’ and ‘c’ different from ‘a’ have still the
same value.
The strict interpretation of postconditions of procedures allows us to
interpret the postconditions as framing conditions. A postcondition of a
procedure contains everything the procedure has modified and all information
needed to derived the things which have not been modified. Postconditions of
procedures are treated as implicit framing conditions.
We need a procedure to put elements at certain positions.
class ARRAY[G] feature put(i:NATURAL, v:G) -- Update (or set) the value `v' at position `i'. require i < capacity note built_in ensure domain = old (domain + {i}) Current[i] = v end end
In order to make the arguments more transparent we use the following
equivalent declaration of the procedure ‘put’.
feature put(a:CURRENT, i:NATURAL, v:G) require i < a.capacity note built_in ensure a.domain = old (a.domain + {i}) Current[i] = v end end
The procedure ‘put’ modifies two functions, the ghost function ‘domain’ and
the element access function represented by the bracket operator ([]). The
verifier extracts the following strict interpretation of this procedure.
feature put(a:CURRENT, i:NATURAL, v:G) require i < a.capacity note built_in ensure domain = old (domain + [a, a.domain + {i}]) ([]) = old (([]) + [[a,i],v]) capacity = old capacity end end
I.e. the procedure put assigns new values to the functions domain and
([]). Since the function capacity is independent of the functions domain and
([]) and the postcondition does not mention neither the function capacity nor
any function which depend on it, the strict postcondition is extracted that
capacity remains unchanged.
There is one interesting thing to note about the procedure put and the
function ([]). The procedure put modifies the function ([]) at just one
position and updates the domain appropriately. The signatures of ([]) and put
are consistent in the sense that they share the same arguments. The procedure
put has just one argument more which is the value to be set. These facts are
sufficient for the compiler to derive that put is an assigner for the function
([]). We can use this to write more readable code. Instead of writing
a.put(i,v)
we can use the more suggestive notation
a[i] := v
It is easy to write a procedure which swaps two elements of an array.
feature swap(a:CURRENT, i,j:NATURAL) require i in a.domain j in a.domain ensure a[i] = old a[j] a[j] = old a[i] end end
Because of the fact that the function ([]) has the assigner command put the
procedure swap needs no explicit implementation. The postcondition is
sufficient because it states that the function ([]) gets new values at the
argument positions [a,i] and [a,j]. The assigner command put can be used to
derive the implementation. The automatically derived implementation looks like
local tmp1, tmp2:G := a[i],a[j] do a.put(i,tmp2) a.put(j,tmp1) end
As for the previous procedures we give the strict interpretation of the
postcondition.
feature swap(a:CURRENT, i,j:NATURAL) require i in a.domain j in a.domain ensure domain = old domain capacity = old capacity ([]) = old (([]) <+ {i -> a[j], j -> a[j]}) end end
In this postcondition we have used a notation which needs some
explanation. In our programming language we can define a function f:A->B by
giving a set of key value pairs.
local f:STRING->NATURAL do ... f := {"one" -> 1, "two" -> 2} -- An illegal assignment f := {"one" -> 1, "two" -> 2, "one" -> 2} ... end
The verifier generates the proof obligation that the same key must have the
same value. Therefore the second assignment in the example is illegal. Let us
look at the proof obligation generated for the second assignment.
a1: "one" = "two" => 1 = 2 a2: "one" = "one" => 1 = 2
The assertion a1 is provable because the antecedent of the implication is
false and from a false antecedent anything can be in the consequent. The
assertion a2 is definitely not provable because it requires the verifier to
prove ‘1=2’ which is not possible.
Going back to the procedure swap we can see that ‘{i->a[j],j->[a[i]}’ is a
function of type NATURAL->G with generates the proof obligation
i = j => a[i] = a[j]
which is trivially valid.
Furthermore in the postcondition we have used the function override operator
‘<+’ which has the following definition for two functions ‘f,g:A->B’.
f <+ g = (x -> if x in g.domain then g(x) elseif x in f.domain then f(x) end)
with the consequences
(f<+g).domain = f.domain + g.domain x in g.domain => (f<+g)(x) = g(x) x in f.domain-g.domain => (f<+g)(x) = f(x)
Using these definitions one can verify that the above mentioned strict
postcondition has the intended meaning.
A function is a computation object which calculates a result given its
arguments. A procedure is a computation object which establishes a
postcondition provided that its precondition is satisfied. Functions and
procedures do not interact with their environment during their lifetime. They
just have an entry point and an exit point. What happens between is not
observable (at least if we abstract the duration).
A process can interact with its environment. A complete program is usually a
process, because in order to have some value it has to interact with its
environment, which can be a user (equipped with mouse, keyboard and monitor) or
a filesystem or other processes. This is the reason why in most operating
systems running programs are called processes.
The interactions between a process and its environment are called events. The
only thing an event can do is “happen”.
In this article we use Tony Hoare’s theory of “Communicating sequential
processes” presented in a book with the same title in 1985. We just adapt the
syntax to fit our programming language.
In the following description we use sets and relations extensively.
Remember: If T denotes a type then T? denotes the type of a predicate over
objects of type T. All objects which satisfy the predicate form a set. Since
the connection between predicates and sets of objects which satisfy the
predicate is so strong, we identify both concepts. I.e. the type T? denotes a
predicate over the type T and a set of objects of type T at the same time.
This notation is extended to relations. The type [T,U] denotes pairs of
objects of type T and type U. Consequently [T,U]? is a predicate with two free
variables or a set of pairs. I.e. we use the type [T,U]? to denote a relation
between objects of type T and objects of type U.
In order to understand the notation in this article fully it might be
convenient to read (at least the basic concepts) of the articles
– Functions, ghost functions and higher order functions
– Predicates as sets
– Complete lattices and closure systems
– Binary relations, endorelations and transitive closures
Consider a simple vending machine which sells chocolates. This vending machine
has two events:
– coin: The event of a coin being inserted in the slot of the machine
– choc: The event of extraction of a chocolate from the dispenser of the
machine
Events are treated as features in our programming language. Their declaration
is very simple
feature coin choc end
I.e. events look like argument-less procedures without body. Since the only
thing an event can do is “happen”, there is neither an implementation nor a
postcondition (but events can have arguments and preconditions as we will see
later).
Having these two events we can define the process which represents a simple
vending machine.
feature vm_choc ensure Process = (coin -> choc -> vm_choc) end end
I.e. a process looks syntactically like a procedure which gives in its
postcondition an expression which states the property of the process. The
variable ‘Process’ represents the process and is just a placeholder for
‘vm_choc’ i.e. for the process to be defined. Therefore the definition states
the equality
vm_choc = (coin -> choc -> vm_choc)
which is a recursive equation. We can expand the recursive definition as often
as we like to get the equations
vm_choc = (coin -> choc -> (coin -> choc -> vm_choc)) vm_choc = (coin -> choc -> (coin -> choc -> (coin -> choc -> vm_choc))) ...
The above definition says that the process ‘vm_choc’ can engage first in the
event ‘coin’ then in the event ‘choc’ and then behaves like
‘vm_choc’. I.e. the process ‘vm_choc’ can engage in the potentially infinite
sequence of events
coin, choc, coin, choc, ...
The prefix operator ‘->’ is a right associative binary operator which has
on its left side an event and on its right side a process. I.e. we have to
‘parse’ the above definition it the following way.
vm_choc ensure Process = (coin -> (choc -> vm_choc)) end
A process like ‘vm_choc’ alone does nothing because events are
interactions. Therefore the process has to be placed into an environment where
it can interact. An environment can be a customer which can engage in the same
events, i.e. a customer having a coin which he inserts into the slot of the
machine and wants to extract a chocolate from its dispenser.
A process can engage in events or refuse events. Initially the vending machine
‘vm_choc’ can engage in the event ‘choc’ and refuses to dispense a
chocolate. After having received the coin it can engage in the event ‘choc’
and refuses to accept a new coin until the chocolate has been dispensed. The
set of all events which a process can accept or refuse is called its
alphabet. The alphabet of a process is a constant through its lifetime. Events
outside its alphabet are not known to a process, i.e. it can neither engage in
nor refuse them.
Since our programming language is strongly typed all objects must have a
type. All event objects like ‘coin’ and ‘choc’ are of type EVENT, all process
objects like ‘vm_choc’ are of type PROCESS.
The vending machine vm_choc has just a linear sequence of events. It does not
offer its customers any choice. The customer just can insert a coin and
extract a chocolate. In the programming language it is easy to express
choice. A vending machine which offers its customers a chocolate or a toffee
after inserting a coin can be defined as
vm_choc_toffee ensure Process = (coin -> ( choc -> vm_choc_toffee | toffee -> vm_choc_toffee)) end
where the operator ‘|’ expresses a choice.
The processes vm_choc and vm_choc_toffee are defined by a recursion equation
of the form ‘p=f(p)’. The definitions
proc ensure Process = f(proc) end proc ensure Process = x: f(x) -- a fresh variable `x' end
are equivalent. With this notation we can express the definition of our two
vending machines in the following form.
vm_choc ensure Process = x: coin -> choc -> x end vm_choc_toffee ensure Process = x: coin -> (choc -> x | toffee -> x) end
I.e. the expression x:f(x) defines a process if ‘f’ is a function of type
PROCESS->PROCESS (Note: The function ‘f’ has to be guarded, i.e. it has to
start with an event. The expression ‘x:x’ is not a proper process expression).
One important aspect of a process is the sequence of events in which it can
engage. At any point in time a process has a history of events in which it has
engaged up to this point in time. We use the type LIST[EVENT] to talk about
the sequence of events and call them traces.
TRACE = LIST[EVENT]
The following are valid traces of vm_choc_toffee
[] [coin, toffee, coin] [coin] [coin, choc, coin choc, coin, toffee, coin]
We can define the set of all traces ts:TRACE? in which a process can engage.
Note that any prefix of a trace is a valid trace. Therefore the set of traces
of a process has to be prefixclosed.
is_prefixclosed(ts:TRACE?): ghost BOOLEAN ensure Result = ts.is_closed({u,v: v.is_prefix(u)}) end
Since the empty trace is a prefix of any trace, the empty trace is contained
in the trace set of any process.
Every traceset defines the set of events which occur in any of its traces
alphabet(ts:TRACE?): ghost EVENT? -- The alphabet of the trace set `ts'. ensure Result = {e: some(t:TRACE) t in ts and e in t} end
With this function we get
vm_choc.traces.alphabet = {coin, choc} vm_choc_toffee.traces.alphabet = {coin, choc, toffee}
where the function ‘traces’ returns the trace set of a process.
The set of all prefixclosed trace sets is a closure system. An arbitrary
traceset does not represent a valid traceset of a process. The closure of a
trace set with respect to the prefix relation is the smallest set which
contains the trace set and is prefixclosed. I.e. the set
{[coin,choc,coin]}
is not a valid traceset. We can generate a valid trace set by closing the set.
{[coin,choc,coin]}.closed(is_prefixclosed) = {[], [coin], [coin,choc], [coin,choc,coin]} -- Note: The expression is_prefixclosed is just a shorthand for -- {ts:TRACE?: ts.is_prefixclosed}
> For details on closures see the article “Complete lattices and closure
systems”.
Let us try to find a closed expression for the trace set of the process
vm_choc.
First we note: If t is a trace then [coin,choc]+t is a trace as well. The
expression {nil}.closed(t->[coin,choc]+t) therefore generates a set of valid
traces of vm_choc. This traceset is not yet prefixclosed. The full set of
valid traces can be generated by the formula
vm_choc.traces = {nil}.closed(t->[coin,choc]+t) .closed(is_prefixclosed) -- Note: t->[coin,choc]+t is the function which maps any trace t -- to the trace [coin,choc]+t -- The type checker of our programming language can resolve this -- ambiguity between function expressions and process expressions
In a similar manner we can give a closed expression for the traces of the
process ‘vm_choc_toffee’.
vm_choc_toffee.traces = {nil} .closed({u,v: v=[coin,choc ]+u or v=[coin,toffee]+u}) .closed(is_prefixclosed)
Note that we have used three possibilities to close a set: (a) close a set
with respect to a closure system, (b) close a set under a function and (c)
close a set under a relation. Let us shortly repeat what these closures
mean. Let c be a set, cs be a set of sets which are a closure system, f be a
function and r a relation. Then we have
c.closed(cs) = ({x:c<=x}*cs).infimum -- least set of cs which contains c c.closed(f) = {c + c.image(f) + c.image(f).image(f) + ... } c.closed(r) = {c + c.image(r) + c.image(r).image(r) + ... } -- Note: '<=' for sets is the subset relation i.e. {x:c<=x} is the set of -- all sets which contain c.
where c.image(f) is the image of the set c under the function f and c.image(r)
is the image of the set c under the relation r.
A specific trace t of a process characterizes in some way the state of a
process. We might be interested in the behaviour of a process after having
engaged in the events of the trace t. Clearly t must be a valid trace of the
process, otherwise this question does not make any sense. The behaviour of a
process after having engaged in t is characterized by a set of traces as well.
We define the function
/ (ts: TRACE?, t:TRACE): ghost TRACE? -- The traceset of a process with traceset `ts' after having -- engaged in the events of the trace `t'. ensure Result = ts.image(u -> u.without_prefix(t)) end
Note that the function ‘image’ for a function f of type A->B is defined as
image(p:A?, f:A->B): ghost B? ensure Result = {b:B: some(a:A) a in f.domain and b=f(a)} end
The initial events of a traceset are defined by the following function.
initials(ts:TRACE?): ghost TRACE? -- The set of initial events of the traceset `ts'. ensure Result = {e: some(u:TRACE) e::u in ts} end
There might be traces in a traceset which do not have any continuation. These
are called endtraces.
endtraces(ts:TRACE?): ghost TRACE? ensure Result = {t: t in ts and ts/t = 0} end
Any process after having engaged in the events of one of its endtraces cannot
continue to engage in events. There are two possible reasons for a process to
reach any of its endtraces: (a) The process has done what it is supposed to do
successfully and has therefore terminated. (b) The process has reached some
deadlock situation. Successful termination or deadlock cannot be
distinguished by the traces of a process.
The possibility of having endtraces reveals the fact that the alphabet of a
traceset as defined above cannot represent the alphabet of the
process. Consider a process with the traceset ts and an endtrace t. Then the
alphabet of the traceset ts/t is empty. This is not inline with the
requirement that the alphabet of a process has to be an invariant during its
lifetime.
If we have a traceset ts of a process we know a lot about the behaviour of the
process. The traceset defines the sequence of all possible events in which the
process can engage.
But we can look at a process as well by asking what it does not do, i.e. what
it refuses to do. E.g. our vending machine for chocolates refuses to dispense
a chocolate at the beginning. It expects first a coin to be inserted. After a
coin has been inserted the machine refuses the insertion of another
coin. Before the insertion of another coin the chocolate has to be extracted.
We define a refusal as a set of events which are refused by a process at a
certain state.
REFUSAL = EVENT?
If the environment offers the events of a refusal then the combination of the
process with its environment cannot continue, it is blocked.
If r is a refusal (i.e. a set of events) and the environment offers a subset s
(i.e. s<=r) then s must be a refusal as well. Therefore at any state the
process has some set of refusals which is downclosed (i.e. with each set it
contains the subsets as well).
The state of a process is characterized by the sequence of events in which it
has engaged up to a certain point in time. The combination of a trace t and a
refusal r i.e. the pair [t,r] is called a failure of the process. We call
[t,r] a failure because the combination of the process with its environment
fails after engaging in the events of t if the environment offers the events
in r.
The set of all trace refusal pairs of a process is called its failure
relation (note that this a relation between traces and set of refusals,
i.e. set of sets).
Our intuition tells us that the refusals have to be in some way a complement
of what the traceset of a process is describing. This stems from the
conjecture that a process can either engage in an event or refuse an event and
both actions being mutually exclusive. This is in fact true for deterministic
processes.
Let us try to find an expression of the set of refusals of a process with the
traceset ts after having engaged in the events of the trace t. The process is
going to accept events of the set (ts/t).initials. The refusal sets have to be
subsets of the complement of (ts/t).initials. But complement with respect to
what? In order to answer this question the alphabet of a process enters the
scene.
Let a be the alphabet of a process described by the traceset ts. In order to
be a valid alphabet it has to be a superset of ts.alphabet. After having
engaged in the events of the trace t the set {r: r <= a-(ts/t).initials} is
the set of refusals which is inline with our intuition about deterministic
processes.
We can use this method to define a function which returns the failure relation
of a deterministic process with the traceset ts and the alphabet a.
failures(ts:TRACE?, a:EVENT?): ghost [TRACE,REFUSAL]? ensure Result = {t,r: t in ts and r <= a - (ts/t).initials} end
The function failures calculates a meaningful failure relation of a process
only if the traceset is prefixclosed and the alphabet is a superset of the
alphabet defined by the traceset.
Note that the relation calculated by the function failures has a very specific
property. Any refusal after engaging in the trace t is a subset of
a-(ts/t).initials which is the greatest refusal after trace t. If fr is the
failure relation calculated by the function ‘failures’ then
t.image(fr).supremum is this greatest refusal. Note that this is not the most
general case. The most general case is that t.image(fr).supremum is not
contained in t.image(fr) which characterizes a non-deterministic process (see
next chapter).
The failure relation of a process contains more information than its
traceset. We can define some useful functions based on the failure relation of
a process.
initials(fr:[TRACE,REFUSAL]?): ghost EVENT? -- The set of the initial events in which a process -- characterized by the failure relation `fr' can engage. ensure Result = fr.domain.initials end refusals(fr:[TRACE,REFUSAL]?): ghost REFUSAL? -- The set of all initial refusals of a process -- characterized by the failure relation `fr'. ensure Result = nil.image(fr) end events(fr:[TRACE,REFUSAL]?): ghost EVENT? -- The set of all events which a process characterized by -- the failure relation `fr' can initially engage in or refuse. ensure Result = fr.initials + fr.refusals.supremum end alphabet(fr:[TRACE,REFUSAL]?): ghost EVENT? -- The set of all events which can occur either in a trace or -- in a refusal of the failure relation `fr'. ensure Result = {e: some(t,r) [t,r] in fr and (e in t or e in r)} end
/ (fr:[TRACE,REFUSAL]?, t:TRACE): ghost [TRACE,REFUSAL]? -- The failure relation of a process characterized by the -- failure relation `fr' after having engaged in `t'. ensure Result = {u,r: t+u in fr.domain and r in (t+u).image(fr)} end
As we have seen above an arbitrary relation between traces and refusals is not
a valid failure relation of a process. With the defined functions we can
formulate the constraints of the failure relation precisely.
is_process(fr:[TRACE,REFUSAL]?): ghost BOOLEAN ensure Result = ( fr.domain.is_prefixclosed and (all(t:TRACE) t.image(fr).is_downclosed) and (all(t:TRACE) t in fr.domain => fr.alphabet = (fr/t).events) ) end
These conditions require that (a) the traceset characterized by the domain of
the relation has to be prefixclosed, (b) each subset of a refusal is a refusal
and (c) the alphabet is an invariant through the lifetime of a process.
It is interesting to note that the set of all failure relations which describe
a valid process is a closure system. I.e. we can use a arbitrary relation fr
between traces and refusals and form a valid one by building the closure
fr.closed(is_process). The relation ‘fr.closed(is_process)’ is the least
relation which contains ‘fr’ and satisfies the requirements to be a valid
process.
The processes we have seen up to now are all deterministic. The environment
has full control over the behaviour via the events it offers. E.g. interacting
with the vending machine vm_choc_toffee the customer can insert a coin and can
decide whether to extract a chocolate or a toffee.
vm_choc_toffee ensure Process = x: coin -> (choc -> x | toffee -> x) end
The choice after inserting the coin is done by the environment.
Let us construct a machine like vm_choc which dispenses only toffees.
vm_toffee ensure Process = x: coin -> toffee -> x end
We can combine vm_choc and vm_toffee with the choice operator.
vm_choc | vm_toffee
> Note: In Tony Hoare’s book about CSP this operator has been displayed as a
small square. Since squares are difficult to display in ASCII we use the |
operator.
The choice operator | suggests that the environment has a choice like in the
machine vm_choc_toffee after inserting the coin. But there is no real choice
here because both processes vm_choc and vm_toffee require a coin to be
inserted at the beginning. However the expression vm_choc|vm_toffee requires
a choice to be made. The only reasonable solution is to say that the process
described by the expression vm_choc|vm_toffee makes this choice
nondeterministically.
Once the choice has been made the process behaves subsequently either like
vm_choc or vm_toffee. The environment can note the consequence of this choice
after having inserted a coin and extracted one of the two possibilities. From
this point on the process behaves deterministic like a vm_choc or a
vm_toffee.
Note that the environment (i.e. a potential customer) has to be prepared for
both possibilities. If the customer insists to extract a toffee and the
process has made the non-deterministic choice to behave like a vm_choc then the
system of process and environment is deadlocked and cannot continue.
Some possible traces of vm_choc|vm_toffee are
{ [], [coin], [coin, choc], [coin, choc, coin, choc, coin, choc], [coin, toffee], [coin, toffee, coin, toffee, coin, toffee], ... }
The traceset of vm_choc|vm_toffee is the union of the tracesets of the
components.
It is interesting to look at the initial events and the refusal set of the
combined machine after having engaged in the first insertion of a coin
((vm_choc|vm_tofffee)/[coin]).initials = {choc,toffee} ((vm_choc|vm_tofffee)/[coin]).refusals = { 0, {coin}, {choc}, {coin,choc}, {toffee}, {coin,toffee} }
The combination might accept the extraction of a chocolate and it might accept
the extraction of a toffee. The internal choice made by the process is not yet
visible to the environment.
The set of refusals does not have a greatest element. The complement of the
initials with respect to the alphabet which is the one element set {coin} is
no longer the greatest refusal like it would be if the process were
deterministic. We can use this observation to formally define deterministic
and non-deterministic processes.
is_deterministic(fr:[TRACE,REFUSAL]?): ghost BOOLEAN ensure Result = all(t,r) require [t,r] in fr ensure r <= fr.alphabet - (fr/t).initials end end
In this basic discussion about non-deterministic processes we have glossed over
an important detail. The expression vm_choc|vm_toffee is illegal because the |
operator requires identical alphabets of both operands. The process vm_choc
has the alphabet {coin,choc} and the process vm_toffee has the alphabet
{coin,toffee}. Therefore the above is valid only if we augment the alphabet of
vm_choc by the set {toffee} and the alphabet of vm_toffee by {choc}. The
augmentation of alphabets of a process is treated formally later. But the
basic semantics is that augmentation does not change the traces. It adds some
refusals to convert the alphabet into the augmented alphabet.
The failure relation does not describe a process completely. The missing
information is obvious if we look at endtraces of a process. By definition an
endtrace has no continuation. A process in this situation can be either
deadlocked or might have terminated successfully.
Termination can be deterministic or nondeterminisitic, i.e. a process might
make some internal choices to terminate or not terminate after engaging in
some trace. In order to treat non-determinism properly for terminations as well
we have to partition the set of traces into
terminations: TRACE? nonterminations: TRACE?
with the obvious constraint
fr.domain = terminations + nonterminations
For a deterministic process we require that both sets are disjoint.
The combination of the failure relation the termination and nontermination
traces is sufficient to describe the behaviour of a process.
Recursive definitions of functions (and processes as we well see later) are
fixpoint equations. The simple example of the recursive definiton of the
factorial function can be used to illustrate this fact.
fact(n:NATURAL): NATURAL ensure Result = if n=0 then 1 else n*fact(n-1) end end
This definition is equivalent to the following assertion.
fact_all: all(n:NATURAL) ensure fact(n) = if n=0 then 1 else n*fact(n-1) end end
This shows the function ‘fact’ appearing on the left hand side and the right
hand side of an equation. In order to see the fixpoint equation better we
derive the function ‘f’ from the definition of ‘fact’.
f(g:NATURAL->NATURAL): NATURAL->NATURAL ensure Result = (n -> if n=0 then 1 else n*g(n-1) end) end
The function ‘f’ transforms any function of type NATURAL->NATURAL to a
function of the same type. Using ‘f’ we can see that the function ‘fact’ has
to satisfy the fixpoint equation
fact_fix: fact = f(fact)
Expanding the definition of ‘f’ and using equality of functions it is evident
that ‘fact_fix’ and ‘fact_all’ are the same assertion.
Mathematically the following question arises: Does the function ‘f’ have
fixpoints? If yes, is the fixpoint unique? If the answer to both questions is
yes, then the fixpoint equation ”fact=f(fact)’ defines the function ‘fact’.
In this article we investigate the question if a function ‘f’ has
fixpoints. The outline of the developed thoughts is the following.
Having a function ‘f’ we can try any element ‘a’ in its domain (in the case of
the example the element is itself a function) and feed it into the function to
get ‘f(a)’. As long as the result is within the domain of ‘f’ we can iterate
this procedure getting the set
cl = {a, f(a), f(f(a)), f(f(f(a))), ... }
The procedure stops if we encounter a fixpoint or an element not in the domain
of ‘f’. The set ‘cl’ is obtained by the closure operation
cl = a.closed(f)
If we are in the domain of a partial order (and the type A->B is a partial
order) and we start at an element ‘a’ where the function ‘f’ is increasing,
then the sequence ‘a, f(a), f(f(a)), …’ is increasing. If ‘a’ is the least
element and is in the domain of ‘f’ this condition is satisfied.
I.e. if we feed the completely undefined function ‘0:NATURAL->NATURAL’ into
‘f’ we get the sequence
cl = {0, f(0), f(f(0)), f(f(f(0))), ... }
For the above example of the factorial we get the following set of functions
(representing functions as a set of key-value pairs).
cl = { {} -- The undefined function {0 -> 1} -- Function defined for one argument {0 -> 1, 1 -> 1} {0 -> 1, 1 -> 1, 2 -> 2} {0 -> 1, 1 -> 1, 2 -> 2, 3 -> 6} {0 -> 1, 1 -> 1, 2 -> 2, 3 -> 6, 4 -> 24} ... }
At each iteration we get a ‘better’ approximation of the factorial
function. It is intuitively clear that this set ‘coverges’ in some sense to the
desired factorial function.
In the following chapters we are going to prove the following facts about such
a closure for the domain of a partial order.
-- module: partial_order feature all(a,c:CURRENT, f:CURRENT->CURRENT) require f.is_monotonic a in f.domain => a in f.prefixpoints ensure a.closed(f) * f.domain <= f.prefixpoints a.is_least(a.closed(f)) a.closed(f).is_chain (some(c) c.is_greatest(a.closed(f)) => a.closed(f).is_finite all(c) c.is_greatest(a.closed(f)) = (c in f.domain => c in f.fixpoints) end -- Note: f.prefixpoints = {x: x in f.domain and x<=f(x)} end
We can phrase the content of the theorems as follows: If the function is
monotonic and the start point is either not in domain of the function or is a
prefixpoint of the function then
– all elements in the closure which are in the domain are prefixpoints as
well
– the start point is the least element of the closure
– the closure is a chain (i.e. all elements ‘a’, ‘b’ of the closure are
related in the sense that ‘a<=b or b<=a’ is valid)
– the existence of a greatest element of the closure implies that the closure
is finite
– the greatest element of the closure (if it exists) is either not in the
domain or it is a fixpoint
If we switch from partial orders to upcomplete partial orders we get the
following stronger results.
-- module: upcomplete_partial_order feature all(f:CURRENT->CURRENT) require f.is_continuous -- 'f' preserves suprema f.domain.is_limitclosed -- Domain is sufficiently large f.prefixpoints /= 0 ensure f.fixpoints /= 0 end end
This law states that any continuous function with sufficiently large domain
having prefixpoints is guaranteed to have fixpoints.
If we have a function ‘f:A->A’ we can apply the function to an argument ‘a’
of type A within its domain and get ‘f(a)’ which is as well of type A. If
‘f(a)’ is in the domain of ‘f’ we can reapply the function getting
‘f(f(a))’. Iterating this procedure we get the set
{a, f(a), f(f(a)), f(f(f(a))), ... }
As long as the result remains in the domain of ‘f’ we can repeat this
procedure forever. We call a set ‘p’ closed under ‘f’ if ‘f’ applied to any of
its members does not leave the set.
Closures have been described in detail in the article “Complete lattices and
closure systems”. Let us repeat some of the results.
local A: ANY feature -- Closed sets and induction
A set ‘p’ is closed under ‘f’ if its image under ‘f’ is a subset of ‘p’.
is_closed(p:A?, f:A->A): ghost BOOLEAN ensure Result = p.image(f) <= p end
The collection of all sets closed under ‘f’ are a closure system.
all(f:A->A) ensure {p:A?: p.is_closed(f)}.is_closure_system end
Being a closure system means that the intersection (aka infimum) of any
collection of closed sets is closed. We can close any set (therefore any one
element set as well) with respect to this closure system. The closure of a set
‘p’ under the function ‘f’ is the least set containing ‘p’ which is closed
under the function ‘f’.
closed(p:A?, f:A->A): ghost A? -- The set `p' closed under the function `f' ensure Result = ({q:A?: p<=q}*{q:A?: q.is_closed(f)}).infimum end closed(a:A, f:A->A): ghost A? -- The one element set {a} closed under the function `f' ensure Result = {a}.closed(f) end
Remember that {q:A?: p<=q} is the set of all sets containing ‘p’. The
expression ‘p.closed(f)’ selects the intersection of all supersets of ‘p’ with
the closed sets. Because the closed sets form a closure system it is
guaranteed that this intersection is closed.
The function ‘closed’ has some nice properties.
all(p,q:A?, f:A->A) ensure closure_1: p.closed(f).is_closed(f) closure_2: p.is_closed(f) => p = p.closed(f) ascending: p <= p.closed(f) monotonic: p <= q => p.closed(f) <= q.closed(f) idempotent: p.closed(f).closed(f) = p.closed(f) end
These rules say that the set generated by ‘p.closed(f)’ is really closed and
every closed set can be obtained by applying the function ‘closed’. The
function ‘closed’ is ascending, monotonic and idempotent.
Whenever we close any element ‘a’ under ‘f’, i.e. forming ‘a.closed(f)’ we can
be sure that the closure of any member of ‘a.closed(f)’ is completely
contained in ‘a.closed(f)’. This claim can be proved by
subclosure: all(a,b:A, f:A->A) require r1: b in a.closed(f) check c1: {b} <= a.closed(f) -- r1 c2: {b}.closed(f) <= a.closed(f).closed(f) -- c1,closure is -- monotonic ensure b.closed(f) <= a.closed(f) -- c2, closure is -- idempotent end
Another quite useful law allows us to decompose a closure.
closure_decomposition: all(p:A?, f:A->A?) ensure p + p.image(f).closed(r) = p.closed(f) end
We prove the forward and backward direction separately.
closure_decomposition_fwd: all(p:A?, f:A->A) check c1: p <= p.closed(f) -- closure is increasing c2: p.image(f) <= p.closed(f) -- closure is closed and contains 'p', -- therefore p.image(f) as well c3: p.image(f).closed(f) <= p.closed(f) -- c2,monotonic,idempotent ensure p + p.image(f).closed(f) <= p.closed(f) -- c1,c3 end
closure_decomposition_bwd: all(p:A?, f:A->A) local r1: pic = p.image(f).closed(f) check c1: p <= p + pic c2: (p+pic).image(f) = p.image(f) + pic.image(f) -- image distributes over union c3: p.image(f) <= pic -- r1, closure is increasing c4: pic.image(f) <= pic -- r1, pic is closed c5: p.image(f) + pic.image(f) <= pic -- c3,c4 c6: (p+pic).image(f) <= pic -- c2,c5 c7: (p+pic).image(f) <= p+pic -- c6 c8: (p+pic).is_closed(f) -- c7, def 'is_closed' c9: p.closed(f) <= p+pic -- c1,c8, closure is -- least set ensure p.closed(f) <= p + p.image(f).closed(f) -- c9,r1 end
In order to proof that all members of a closed set ‘p.closed(f)’ satisfy a
certain property we can use the following induction principle.
induction: all(p,q:A?, f:A->A) require p <= q q.is_closed(f) ensure p.closed(f) <= q end
We can apply this principle by defining a set ‘q’ whose elements have the
desired property. In order to prove that all elements of ‘p.closed(f)’ have
this property we need to prove that all members of ‘p’ have this property and
that the set ‘q’ is closed under ‘f’.
We can prove this induction principle with the following reasoning.
all(p,q:A?, f:A->A) require r1: p <= q r2: q.is_closed(f) check c1: p.closed(f) <= q.closed(f) -- r1, monotonic ensure p.closed(f) <= q -- c1, closure_2 end
end -- Closed sets and induction
An element ‘a’ is a fixpoint of the function ‘f’ if it is in the domain of ‘f’
and ‘a=f(a)’ is valid.
local A: ANY feature -- Fixpoints fixpoints(f:A->A): ghost A? -- The set of fixpoints of `f'. ensure Result = {a: a in f.domain and a=f(a)} end end -- Fixpoints
By definition all fixpoints are in the domain of the function.
In the domain of a partial order we can define prefixpoints and postfixpoints.
local A: PARTIAL_ORDER feature -- Pre- and postfixpoints prefixpoints(f:A->A): ghost A? -- The set of prefixpoints of `f'. ensure Result = {a: a in f.domain and a <= f(a)} end postfixpoints(f:A->A): ghost A? -- The set of postfixpoints of `f'. ensure Result = {a: a in f.domain and f(a) <= a} end end
The set of prefixpoints is the part of the domain where the function is
ascending and the set of postfixpoints is the part of the domain where the
function is descending. Clearly a fixpoint must be a prefixpoint and a
postfixpoint i.e. ‘f.fixpoints = f.prefixpoints*f.postfixpoints’.
local A,B: ANY feature -- Injections and finite sets
An injective function is one-to-one
is_injective(f:A->B): ghost BOOLEAN -- Is the function `f' one-to-one ensure Result = all(a,b:A) require {a,b} <= f.domain f(a) = f(b) ensure a = b end end
Clearly every domain restriction of an injective function is again an
injective function
all(p:A?, f,g:A->B) require f.is_injective ensure g <= f => g.is_injective (p<:f).is_injective end
A set ‘p:A?’ is finite if all functions mapping ‘p’ to a proper subset of ‘p’
are not injective.
is_finite(p:A?): ghost BOOLEAN ensure Result = all(f:A->A) require f.domain = p f.range < p ensure not f.is_injective end end
This definition is sometimes illustrated as the pigeonhole principle. Imagine
n pigeonholes and n+m pidgins where ‘m>0’. If all the n+m pigeons are in the
n pidgoenholes then at least one pigeonhole has more than one pigeon.
The set {i: i<n} is definitely a proper subset of {i: i<n+m}. If all pigeons
fly into the holes we have a mapping from the set of pigeons {i: i<n+m} to
the set of pigeonholes {i: i<n}. The fact that there is at least one hole
with more than one pigeon expresses the fact that the mapping cannot be
one-to-one.
Let us demonstrate that this definition of finiteness is inline with our
intuition about finiteness. First of all we expect the empty set to be finite.
all(p:A?) require r1: p = 0 check all(f:A->A) require r2: f.domain = p r3: f.range < p check c1: f.range <= 0 -- r3 c2: f.range /= 0 -- r3 c3: f.range = 0 -- c1, 0 is least element ensure not f.is_injective -- c2,c3, contradiction end ensure p.is_finite end
Furtermore we expect if we add an element ‘a’ to finite set ‘p’ that the set
remains finite.
We prove this by assuming the opposite i.e. ‘p+{a}’ is not finite. Then there
exists an injective function which maps its domain to a proper subset. The
domain restriction to ‘p’ yields an injective function which maps ‘p’ to a
proper subset of ‘p’. Since ‘p’ is finite such a map must not exist therefore
we have a contradiction.
all(a:A, p:A?) require r1: p.is_finite r2: a /in p check c0: require r3: not (p + {a}).is_finite check c1: some(f:A->A) f.domain = p+{a} and f.range < f.domain and f.is_injective c2: all(f:A->A) require r4: f.domain = p + {a} r5: f.range < f.domain r6: f.is_injective local r7: g = p<:f check c3: g.is_injective -- r7,r6 c4: g.domain = p -- r4,r7 c5: g.range = f.range - {f(a)} -- r4,r7 c6: f.range - {f(a)} < f.domain - {a} -- r4,r6 c7: g.range < g.domain -- c5,c6,r4 c8: not g.is_injective -- c4,c7,r1 ensure False -- c3,c8, contradiction end ensure False -- c1,c2 end ensure (p + {a}).is_finite -- c0 end
end -- Injections and finite sets
All code of this chapter is understood to be in the module ‘partial_order’,
i.e. the type CURRENT stands for any type which represents a partial order.
-- module: partial_order feature -- Generated sets
We investigate sets generated by the expression ‘a.closed(f)’ where ‘a’ is the
starting point of the generated set and ‘f’ is a function. The starting point
‘a’ is a prefixpoint of ‘f’ or not in the domain and the function ‘f’ is
monotonic.
The first fact we prove is the statement that all elements of ‘a.closed(f)’
which are in the domain of ‘f’ are prefixpoints provided that the starting
point ‘a’ is a prefixpoint or is not in the domain of ‘f’.
The crucial point in the proof is the following: Whenever some element ‘b’ in
‘a.closed(f)’ is a prefixpoint, then by definition of a prefixpoint
‘b<=f(b)’. By monotonicity of ‘f’ we get ‘f(b)<=f(f(b))’ provided that ‘f(b)’
is in the domain of ‘f’. I.e. the property of being a prefixpoint transfers
from one element to the next as long as there is a next element.
The following proof defines a set ‘p’ which contains all elements of the
closure which are either prefixpoints or not in the domain. The proof shows
that the set ‘p’ is closed under ‘f’. Some technicalities are necessary to
treat the case that an element is not in the domain of ‘f’.
all_prefixpoints: all(a:CURRENT, f:CURRENT->CURRENT) require r1: f.is_monotonic r2: a in (f.prefixpoints + (-f.domain)) local r3: p = s.closed(f) * (f.prefixpoints + (-f.domain)) check c1: {s} <= p -- r3,r2 c2: all(b:CURRENT) require r4: b in p r5: b in f.domain check c3: f(b) in a.closed(f) -- r4,r5,r3 c4: b<=f(b) -- r4,r5,r3 c5: require r6: f(b) in f.domain check c6: f(b) <= f(f(b)) -- c4,r1,r6 ensure f(b) in f.prefixpoints -- r6,c6 end ensure f(b) in p -- c3,c5 end c7: p.is_closed(f) -- c2 c8: a.closed(f) <= p -- c1,c7,induction c9: a.closed(f) <= f.prefixpoints + (-f.domain) -- c8,r3 ensure a.closed(f)*f.domain <= f.prefixpoints -- c9 end
In this chapter we prove that ‘a’ is the least element of ‘a.closed(f)’. In
order to prove this we have to show that all elements of ‘a.closed(f)’ are
above or equal ‘a’.
We want to use induction and define a set ‘p’ which contains all elements of
‘a.closed(f)’ which are above or equal ‘a’. We show that ‘a’ is in this set
(which is trivial) and that the set is closed under ‘f’.
feature -- Start element is least element
start_is_least: all(s:CURRENT, f:CURRENT->CURRENT) require r1: f.is_monotonic r2: a in (f.prefixpoints + (-f.domain)) local r3: p = a.closed(f)*{x:a<=x} check c1: all(b:CURRENT) require r4: b in p r5: b in f.domain check c2: b in f.prefixpoints -- r4,r5,lemma_2 c3: b<=f(b) -- c2 c4: s<=f(b) -- r4,c3,transitivity ensure f(b) in p -- c4,r3 end c5: p.is_closed(f) -- c1 c6: a.closed(f) <= p -- c5, 'a in p', induction ensure a.is_least(a.closed(f)) -- c6,r3 end
In order to prove that ‘a.closed(f)’ is a chain we need one more intermediate
step. We have seen above that for any ‘b’ within the closure the subclosure
‘b.closed(f)’ is completely contained within the closure. I.e. we can split
‘a.closed(f)’ into the two disjoint parts
a.closed(f) = b.closed(f) + a.closed(f)-b.closed(f)
Since we know that all elements of ‘b.closed(f)’ are above or equal ‘b’ we can
try to prove that all elements of ‘a.closed(f)-b.closed(f)’ are strictly below
‘b’.
In order to prove this we put all elements ‘b’ of the closure where
‘a.closed(f)-b.closed(f)’ contains only elements strictly below ‘b’ into a set
‘p’ and prove that ‘a’ is within the set and the set is closed under ‘f’.
split_closure: all(a:CURRENT, f:CURRENT->CURRENT) require r1: f.is_monotonic r2: a in (f.prefixpoints + (-f.domain)) local r3: pa = a.closed(f) r4: p = {b: pa-b.closed(f)<={x:x<b}}*pa check c1: a in p -- r3,r4 c2: all(b) require r5: b in p r6: b in f.domain check c3: b=f(b) or b/=f(b) c4: require r7: b/=f(b) check c5: b<f(b) -- r7, r1, all elements in pa and in -- the domain of f are prefixpoints c6: f(b) in pa -- r5,r4,r6 c7: b.closed(f) = {b}+f(b).closed(f) c8: f(b).closed(f) = b.closed(f)-{b} -- c7,r7 c9: pa-f(b).closed(f) = pa-b.closed(f)+{b} -- c8, lemma_2b c10: {b} <= {x:x<f(b)} -- c5 c11: {x:x<b} <= {x:x<f(b)} -- c5 c11: pa-f(b).closed(f)<={x:x<f(b)} -- c8,r5,r4,c5,c10 ensure f(b) in p -- c11,r4 end ensure f(b) in p -- c4,c8,r3 end c12: p.is_closed(f) -- c2 c13: a.closed(f) <= p -- c1,c12,induction ensure a.closed(f) <= {b: a.closed(f)-b.closed(f) <= {x:x<b}} -- c13,r3,r4 end
Having the theorem ‘split_closure’ it is straightforward to prove that the
closure is a chain since for all elements in the closure there are only
elements above or equal or strictly below in the closure.
all(a:CURRENT, f:CURRENT->CURRENT) require r1: f.is_monotonic r2: a in f.prefixpoints check c1: all(b,c:CURRENT) require r3: b in a.closed(f) r4: c in a.closed(f) check c2: c in a.closed(f)-b.closed(f) or c in b.closed(f) c3: require r5: c in a.closed(f)-b.closed(f) ensure c<b -- r3, split_closure end c4: require r6: c in b.closed(f) check c5: b.is_least(b.closed(f)) -- r6 ensure b<=c -- c5 end ensure b.is_related(c) -- c2,c3,c4 end ensure a.closed(f).is_chain end
Note that being a chain implies being a directed set.
The closure ‘a.closed(f)’ has a greatest element if it contains one element
which is either not in the domain of ‘f’ or is a fixpoint of ‘f’. We expect a
closure with a greatest element to be finite.
We prove this claim by first verifying that set of all elements of
‘a.closed(f)’ below or equal a certain element ‘b’ is finite. The proof is
done by induction. The crucial point is that for any element ‘b’ within the
closure the set of elements below or equal ‘f(b)’ has one element more than
the set of elements below or equal ‘b’, namely ‘f(b)’. If the set of elements
below or equal ‘b’ is finite then the set of elements below or equal ‘f(b)’
has to be finite as well because it contains just one more element.
all(a,b:CURRENT, f:CURRENT->CURRENT) require r1: f.is_monotonic r2: a in (f.prefixpoints + (-f.domain)) r3: b in a.closed(f) local r4: p = {b: ({x:x<=b}*a.closed(f)).is_finite}*a.closed(f) check c1: {x:x<=a} * a.closed(f) = {a} -- r1,r2,'a' is least c2: {a}.is_finite -- '{a} = 0 + {a}' c3: a in p -- c1,c2 c4: all(b:CURRENT) require b in p b in f.domain check b in a.closed(f) f(b) in a.closed(f) {x:x<=f(b)}*a.closed(f) = {x:x<=b}*a.closed(f) + {f(b)} ({x:x<=b}*a.closed(f)).is_finite ({x:x<=f(b)}*a.closed(f)).is_finite ensure f(b) in p end c5: p.is_closed(f) ensure ({x: x<=b}*a.closed(f)).is_finite -- r3,c3,c5,induction end
As long as there is an element ‘c’ in the closure ‘a.closed(f)’ which is in
the domain of ‘f’ and which is not a fixpoint the element ‘f(c)’ is as well in
the closure and strictly above ‘c’. Therefore ‘c’ cannot be the greatest
element of the closure. On the other hand if ‘c’ is a fixpoint of ‘f’ or not
in the domain of ‘f’ then there is no next element. Therefore ‘c’ is the
greatest element of the closure.
Therefore we claim that and element ‘c’ of the closure is the greatest element
if and only if it is either not in the domain of ‘f’ or is a fixpoint of ‘f’.
In order to prove this claim we have to show the forward and the backward
direction. First the forward direction.
greatest_fwd: all(a,c:CURRENT, f:CURRENT->CURRENT) require r1: f.is_monotonic r2: a in f.prefixpoints r3: c in a.closed(f) r4: c in f.domain => c in f.fixpoints check c0: c.closed(f) = {c} -- r4 c1: all(b:CURRENT) require r5: b in a.closed(f) check c2: b in a.closed(f)-c.closed(f) or b in c.closed(f) -- r5 c3: b in a.closed(f)-c.closed(f) => b<c -- r3,'split' c4: b in c.closed(f) => b=c -- c0 ensure b <= c -- c2,c3,c4 end ensure c.is_greatest(a.closed(f)) -- c1,r3 end
Then the backward direction.
greatest_bwd: all(a,c:CURRENT, f:CURRENT->CURRENT) require r1: f.is_monotonic r2: a in f.domain => a in f.prefixpoints r3: c.is_greatest(a.closed(f)) check c1: c in a.closed(f) -- r3 c2: a.closed(f) <= c -- r3 c3: require r4: c in f.domain check c4: f(c) in a.closed(f) -- r4,c1 c5: c <= f(c) -- r4,c1 c6: f(c) <= c -- c4,c2 ensure c in f.fixpoints -- c5,c6 end ensure c in f.domain => c in f.fixpoints -- c3 end
Combining the two assertions we get.
greatest: all(a,c:CURRENT, f:CURRENT->CURRENT) require f.is_monotonic a in f.domain => a in f.prefixpoints ensure c.is_greatest(a.closed(f)) = (c in f.domain => c in f.fixpoints) -- greatest_fwd/bwd end
end -- Generated sets
Now we switch from partial orders to upcomplete partial orders.
-- module: upcomplete_partial_order
It is always interesting to look at functions which preserve the structure. In
partial orders we have monotonic maps which preserve the order structure. For
upcomplete partial orders we can define continuous maps. A function is
continuous if it preserves suprema.
local A,B: CURRENT feature -- Continuous maps
is_continuous(f:A->B): ghost BOOLEAN ensure Result = all(d:CURRENT?) require d.is_directed d+{d.supremum} <= f.domain ensure f(d.supremum).is_supremum(d.image(f)) end end
We claim that this property is strong enough to preserve the order structure
as well. I.e. we state that any continuous map is order preserving.
all(f:A->B) require r1: f.is_continuous check c1: all(a,b:A) require r2: {a,b} <= f.domain r3: a <= b check c2: b.is_supremum({a,b}) -- r3 c3: f(b).is_supremum({f(a),f(b)}) -- c2,r1 c4: {f(a), f(b)} <= f(b) -- c3, supremum is -- upper bound ensure f(a) <= f(b) -- c4 end ensure f.is_monotonic -- c1 end
end -- Continuous maps
feature -- Fixpoints
In order to study fixpoints we have to look at functions of the type
f:CURRENT->CURRENT. Let us first define sets which are closed in the sense
that they contain all directed sets completely if they have some elements in
common and that they include the suprema of contained directed sets.
is_limitclosed(p:CURRENT?): ghost BOOLEAN ensure Result = all(d:CURRENT?) require d.is_directed d * p /= 0 ensure (d + {d.supremum}) in p end end
The central fixpoint law of continuous functions reads like: Whenever a
continuous map with sufficiently large domain has prefixpoints, it has
fixpoints as well. We state this law formally.
some_fixpoints: all(f:CURRENT->CURRENT) require f.is_continuous f.domain.is_limitclosed f.prefixpoints /= 0 ensure f.fixpoints /= 0 -- least_fixpoint below end
We are going to prove this law by constructing the least fixpoint of such a
function i.e. we are going to prove
least_fixpoint: all(a:CURRENT, f:CURRENT->CURRENT) require f.is_continuous a in f.prefixpoints a.closed(f) + {a.closed(f).supremum} <= f.domain ensure a.closed(f).supremum.is_least(f.fixpoints * {x:a<=x}) -- lemma_fix_3 below end
In order to prove this claim we show first that ‘a.closed(f)’ and
‘a.closed(f).image(f)’ have the same supremum.
lemma_fix_1: all(a:CURRENT, f:CURRENT->CURRENT) require a in f.prefixpoints f.is_continuous a.closed(f) + {a.closed(f).supremum} <= f.domain ensure a.closed(f).image(f).supremum = a.closed(f).supremum end
Having this we can demonstrate that the supremum of ‘a.closed(f)’ is a
fixpoint.
lemma_fix_2: all(a:CURRENT, f:CURRENT->CURRENT) require r1: a in f.prefixpoints r2: f.is_continuous r3: a.closed(f) + {a.closed(f).supremum} <= f.domain local r4: s = a.closed(f).supremum check c1: a.closed(f).is_directed -- r1, because it is a chain c2: f(s) = a.closed(f).image(f).supremum -- r2,c1 = a.closed(f).supremum -- lemma_fix_1 = s -- r4 ensure a.closed(f).supremum in f.fixpoints end
But we want to prove that the supremum of ‘a.closed(f)’ is the least fixpoint
above ‘a’. In order to prove this we show that any fixpoint ‘z’ above ‘a’ is
an upper bound of ‘a.closed(f)’. Because the supremum is the least upper bound
it follows that the supremum is the least fixpoint. In order to show that any
fixpoint ‘z’ is an upper bound of ‘a.closed(f)’ we use induction.
lemma_fix_3: all(a:CURRENT, f:CURRENT->CURRENT) require r1: a in f.prefixpoints r2: f.is_continuous r3: a.closed(f) + {a.closed(f).supremum} <= f.domain local r4: s = a.closed(f).supremum check c1: all(z:CURRENT) require r5: z in (f.fixpoints * {x: a<=x}) local r6: p = {b: b<=z}*a.closed(f) check c2: a in p c3: all(b:CURRENT) require r7: b in f.domain r8: b in p check c4: b <= z -- r8,r6 c5: f(b) <= f(z) -- c4,r7,r2 c6: f(b) <= z -- c5,r5 c7: f(b) in a.closed(f) -- r7,r8,r6 ensure f(b) in p -- c6,c7,r6 end c8: p.is_closed(f) -- c3 c9: a.closed(f) <= z -- c2,c8,r6,induction ensure s <= z end ensure a.closed(f).supremum.is_least(f.fixpoints * {x:a<=x}) -- c1 end
end -- Fixpoints
Why study complete partial orders?
Let us look at some simple examples. Everybody knows the recursive definition
of the factorial function. In our programming language the definition looks
like:
factorial(n:NATURAL): NATURAL ensure Result = if n=0 then 1 else n*factorial(n-1) end end
But this is not a classical definition. A classical definition of a function
defines a function in terms of simpler functions. This is a recursive
definition, i.e. we use the function to define the function.
From our intuition we know that this recursive function is well behaved. If we
call the function ‘factorial’ and enter the recursive branch, we know that the
recursive call is with a smaller argument. Finally the recursion will
terminate and the function returns some value.
From a mathematical point of view the above recursive definition is a property
which the function ‘factorial’ has to satisfy. I.e. the definition says that
the function ‘factorial’ has to satisfy the following property.
all(n:NATURAL) ensure factorial(n) = if n=0 then 1 else n*factorial(n-1) end end
Two questions arise naturally from such a property.
– Is there a function which satisfies this property?
– Is this function unique?
The property states that the function ‘factorial’ and the function ‘n -> if
n=0 then 1 else n*factorial(n-1) end’ are the same function. We can express
this better if we define the mapping
g(f:NATURAL->NATURAL): NATURAL->NATURAL ensure Result = (n -> if n=0 then 1 else n*f(n-1) end) end
which maps one function of type NATURAL->NATURAL to another function of the
same type. Now the property which ‘factorial’ has to satisfy reads
all(n:NATURAL) ensure factorial(n) = g(factorial)(n) end
or simpler
factorial = g(factorial)
I.e. the function ‘factorial’ has to be a fixpoint of the function ‘g’.
We can feed any function as an argument into the function ‘g’. For any
function ‘f’ we get
all(f:NATURAL->NATURAL) ensure g(f).domain = {0} + f.domain.image(n->n+1) end
i.e. ‘g(f).domain’ is zero plus the domain of ‘f’ shifted one
up. I.e. ‘g(f).domain’ has more elements than ‘f.domain’ (but is not
necessarily a superset).
If we feed the completely undefined function ‘0’ into ‘g’ we get a function
whose domain has one element. We can iterate this
g(0).domain = {0} g(g(0)).domain = {0,1} g(g(g(0))).domain = {0,1,2} ...
i.e. the set
(0).closed(g) = {0, g(0), g(g(0)), g(g(g(0))), ... }
is a chain. Any chain of functions has a supremum as we will see below. The
supremum of this chain is the fixpoint of ‘g’.
The theory behind complete partial order gives a lot of mathematical tools to
decide if a fixpoint of a function exists, if it is unique etc. In order to
apply the theory to functions we have to verify that the category of functions
is a complete partial order. In this paper we prove that functions form a
complete partial order. The properties of a complete partial order will be
studied in a different article.
The content of this article is based on the results of the articles
– Predicates as sets
– Complete lattices and closure systems
– Binary relations, endorelations and transitive closures.
Let us recall shortly the basic definitions of a partial order.
-- module: partial_order deferred class PARTIAL_ORDER end feature -- Basic definitions and axioms <= (a,b:CURRENT): BOOLEAN deferred end < (a,b:CURRENT): BOOLEAN ensure Result = (a<=b and a/=b) end >= (a,b:CURRENT): BOOLEAN ensure Result = b<=a end > (a,b:CURRENT): BOOLEAN ensure Result = b<a end all deferred ensure (<=).is_reflexive (<=).is_transitive (<=).is_antisymmtric end end -- Basic definitions
Two elements ‘a’ and ‘b’ of a partial order are related if either ‘a<=b’ or
‘b<=a’ is valid.
feature -- Related elements is_related(a,b:CURRENT): BOOLEAN ensure Result = (a<=b or b<=a) end end
For two related elements the maximum (and the minimum) is defined
maximum(a,b:CURRENT): CURRENT require a.is_related(b) ensure Result = if a<=b then b else a end end end -- Related elements
An element ‘a’ is an upper bound of a set ‘p’ if it dominates all elements in
‘p’. An element ‘a’ is a lower bound of a set ‘p’ if all elements of ‘p’
dominate ‘a’.
feature -- Upper and lower bounds <= (p:CURRENT?, a:CURRENT): ghost BOOLEAN -- Is `a' an upper bound of the set `p'? ensure Result = all(x) x in p => x<=a end <= (a:CURRENT, p:CURRENT?): ghost BOOLEAN -- Is `a' a lower bound of the set `p'? ensure Result = all(x) x in p => a<=x end end -- Upper and lower bounds
A set is directed if it is nonempty and contains for each pair of elements an
upper bound of this pair.
feature -- Directed sets and chains is_directed(p:CURRENT?): ghost BOOLEAN -- Is the set `p' nonempty and has for each pair -- of elements an upper bound for these elements? ensure Result = (p/=0 and all(a,b) {a,b}<=p => some(c) {a,b}<=c and c in p) end -- Note {a,b}<=p means that {a,b} is a subset of p -- {a,b}<=c means that c is an upper bound of {a,b}
A set is a chain if each two elemens ‘a’ and ‘b’ are releated
is_chain(p:CURRENT?): ghost BOOLEAN ensure Result = all(a,b) {a,b}<=p => a.is_related(b) end
Clearly every nonempty chain is a directed set because for each pair the
maximum of both elements is an upper bound for both.
all(p:CURRENT?) require r1: p /= 0 r2: p.is_chain check c1: all(a,b:CURRENT) require r3: {a,b}<=p check c2: a.is_related(b) -- r2,r3 c3: {a,b}<=maximum(a,b) -- definition 'maximum' ensure some(c:CURRENT) {a,b}<=c -- c3 end ensure p.is_directed -- r1,c1 end end -- Directed sets and chains
An element ‘s’ is a supremum of a set ‘p’ if it is the least upper bound of
‘p’. In order to define a supremum we need the following definitions.
feature -- Supremum upper_bounds(p:CURRENT?): ghost CURRENT? -- The set of upper bounds of the set `p'. ensure Result = {x: p<=x} end is_least(a:CURRENT, p:CURRENT?): ghost BOOLEAN -- Is `a' the least element of the set `p'? ensure Result = (a in p and a<=p) end is_supremum(a:CURRENT, p:CURRENT?): ghost BOOLEAN -- Is `a' the supremum of the set `p'? ensure Result = a.is_least(p.upper_bounds) end
The least element of a set (if it exists) is unique.
all(a,b:CURRENT, p:CURRENT?) require a.is_least(p) b.is_least(p) ensure a=b end
This implies that a supremum is unique if it exists
all(a,b:CURRENT, p:CURRENT?) require a.is_supremum(p) b.is_supremum(p) ensure a=b end end -- Supremum
The dual notions like ‘is_greatest, ‘is_infimum’ etc. can be defined in a
similar manner.
A function of type A->B where A and B are partial orders is monotonic or
orderpreserving if ‘a<=b’ implies ‘f(a)<=f(b)’.
local A: CURRENT B: CURRENT feature -- Maps is_monotonic(f:A->B): ghost BOOLEAN -- Is the function `f' order preserving ensure Result = all(a,b:A) {a,b}<=f.domain => a<=b => f(a)<=f(b) end
Monotonic maps preserve upper/lower bounds and least elements
all(a:A, p:A?, f:A->B) require f.is_monotonic a in f.domain check -- Proof: expand the definitions of '<=' and 'image' and -- use the elimination law of existential quantification ensure a<=p => f(a)<=p.image(f) p<=a => p.image(f)<=f(a) end
all(a:A, p:A?, f:A->B) require f.is_monotonic a in f.domain a.is_least(p) check -- Proof: expand the definitions of 'is_least' and 'image' and -- use the elimination law of existential quantification ensure f(a).is_least(p.image(f)) end
end -- Maps
A set of elements of a partial order may have a supremum or not. Furthermore a
least element of a partial order may exist or not.
In a complete partial order each directed set has a supremum. We call this
order and upcomplete partial order because the dual notion of a downcomplete
partial order is possible as well (where filtered set/infimum is used instead
of directed set/supremum).
An upcomplete partial order has the following basic definition.
-- module: upcomplete_partial_order deferred class UPCOMPLETE_PARTIAL_ORDER inherit PARTIAL_ORDER end feature -- Basic functions and axioms supremum(p:CURRENT?): ghost CURRENT require p.is_directed deferred end all(a:CURRENT, p:CURRENT?) deferred ensure least: 0 <= a supremum: p.is_directed => p.supremum.is_supremum(p) end end
The type FUNCTION[A,B] defines the partial functions from objects of type A to
objects of type B. The type A->B is a shorthand for FUNCTION[A,B]. Tuples can
be used to specify functions with multiple arguments and return values
(e.g. [A1,A2,…]->[B1,B2,…] which is a shorthand for FUNCTION[
[A1,A2,…],[B1,B2,…] ]).
-- module: function A: ANY B: ANY immutable class FUNCTION[A,B] inherit ghost UPCOMPLETE_PARTIAL_ORDER end
Each function ‘f’ has a domain ‘f.domain’ and for arguments ‘a’ within its
domain (i.e. ‘a in f.domain’) the value ‘f(a)’ is defined (instead of ‘f(a)’
we can use the notation ‘a.f’ as well).
feature domain(f:CURRENT): ghost A? -- The domain of the function. note built_in end () (f:CURRENT, a:A): B -- The value of the function at `a' (written f(a) or a.f). require a in f.domain note built_in end range(f:CURRENT): ghost B? -- The range of the function `f'. ensure Result = {b: some(a:A) a in f.domain and f(a)=b} end end
The partial functions from A to B form a partial order. The function ‘f’ is
less or equal than the function ‘g’ if the domain of ‘f’ is a subset of the
domain of ‘g’ and the values of ‘f’ and ‘g’ are the same within the common
domain.
feature <= (f,g:CURRENT): ghost BOOLEAN -- Is the domain of `f' contained within the domain of `g' and -- have both functions the same values within the common domain? ensure Result = (f.domain<=g.domain and all(a:A) a in f.domain => f(a)=g(a)) end = (f,g:CURRENT): ghost BOOLEAN -- Do `f' and `g' define the same function. ensure Result = (f<=g and g<=f) end all ensure -- Proofs: expansion of definitions (<=).is_reflexive (<=).is_transitive (<=).is_antisymmetric end end
It is easy to prove that ‘<=’ is reflexive, transitive and antisymmetric.
The bottom element of the type A->B is the function which is has an empty
domain, i.e which is not defined for any argument.
feature -- Bottom element 0: CURRENT ensure Result.domain = 0 end
It is evident that ‘0’ is the least function with respect to the defined
order.
all(f:CURRENT) ensure 0 <= f -- trivial end end -- Bottom element
We can map each function of type A->B to a relation of type [A,B]?.
feature -- relations relation(f:CURRENT): ghost [A,B]? -- The relation defined by the function `f'. ensure Result = {a,b: a in f.domain and b=f(a)} end
Clearly the relation defined by the function ‘f’ is functional and the domain
of the function conicides with the domain of the relation and the range of the
function conicides with the range of the relation. Furthermore the function
‘relation’ which maps functions to relations is monotonic
all(f:CURRENT) ensure f.domain = f.relation.domain f.range = f.relation.range f.relation.is_functional relation.is_monotonic end
For any functional relation ‘r’ we can reconstruct the corresponding
function. A relation is functional if the image of each element in its domain
is unique. By expanding the definition of ‘is_functional’ we get the following
assertion.
all(r:[A,B]?, a:A) require r1: r.is_functional r2: a in r.domain ensure exist: some(b:B) [a,b] in r -- r2, def 'domain' unique: all(x,y:B) [a,x] in r => [a,y] in r => x=y -- r1, def 'is_functional end
I.e. for each object ‘a’ in the domain of ‘r’ there exists an image under ‘r’
and the image is unique. Having this we can define a function ‘value’ which
returns this unique value.
value(r:[A,B]?, a:A): ghost B -- The unique image of `a' under the functional relation `r'. require r.is_functional a in r.domain ensure [a,Result] in r end
With this function we can define another function which maps a functional
relation ‘r’ back to its function.
function(r:[A,B]?): ghost CURRENT -- The function defined by the functional relation `r'. require r.is_functional ensure Result = (a -> r.value(a)) end
Clearly ‘function’ is the inverse of ‘relation’. Furthermore ‘function’ is
monotonic.
all(f:CURRENT?, r:[A,B]?) ensure f = f.relation.function r.is_functional => r.function.relation=r function.is_monotonic end
end -- relations
A function is greater than another function if it has a greater domain and
within the common domain both functions have the same values for the same
arguments. If we have two functions ‘f’ and ‘g’ we might want to build the
union of the two functions so that the union has the union of the domains of
‘f’ and ‘g’. It is intuitively clear that such a union is possible only if ‘f’
and ‘g’ do not have different values for arguments within their common domain.
In order to define union of functions we first define the domain restriction
for a function.
feature -- Union of functions <: (d:A?, f:CURRENT): ghost CURRENT -- The function `f' restricted to the domain `d*f.domain'. ensure Result <= f Result.domain = d*f.domain end
With a domain restriction we can split the relation of a function into two
disjoint parts.
all(d:A?, f:CURRENT) ensure (d<:f).relation * ((-d)<:f).relation = 0 end
Furthermore two subsequent domain restrictions are equivalent to the domain
restriction of the intersection (this implies that two subsequent domain
restrictions are commutative).
all(d,e:A?, f:CURRENT) check c1: (d<:e<:f).domain = d*e*f.domain ensure e1: d<:e<:f = (d*e)<:f -- c1 e2: d<:e<:f = e<:d<:f -- e1 end -- Note: The operator '<:' is right associative because it ends with -- a colon.
We say that two functions ‘f’ and ‘g’ are consistent if their restriction to
the intersection of their domains define the same function.
is_consistent(f,g:CURRENT): ghost BOOLEAN ensure Result = (g.domain<:f = f.domain<:g) end
Intuitively two functions are consistent if they don’t have any conflicting
values.
If two functions are consistent then the union of their relations is
functional.
all(f,g:CURRENT, r:[A,B]?) require r1: f.is_consistent(g) local r2: r = f.relation + g.relation check c1: all(a:A, x,y:B) require r3: [a,x] in r r4: [a,y] in r check c2: a in f.domain-g.domain or a in g.domain-f.domain or a in f.domain*g.domain ensure x=y -- r1,r2,r3,r4,c2 case split end c2: r.is_functional ensure (f.relation+g.relation).is_functional -- c2 end
Having this we can define the union of two consistent functions
+ (f,g:CURRENT): ghost CURRENT require f.is_consistent(g) ensure Result = (f.relation+g.relation).function end
On the other hand if two functions have an upper bound then they are
consistent.
all(f,g,h:CURRENT) require r1: f<=h r2: g<=h check c1: f = f.domain<:h -- r1 c2: g = g.domain<:h -- r2 c3: g.domain<:f = (g.domain*f.domain)<:h -- c1 c4: f.domain<:g = (f.domain*g.domain)<:h -- c2 c5: g.domain<:f = f.domain<:g -- c3,c4 ensure f.is_consistent(g) -- c5 end
Therefore any two functions of a directed set are consistent
all(d:CURRENT?, f,g:CURRENT) require d.is_directed f in d g in d check some(h) h in d and {f,g}<=h ensure f.is_consistent(g) end
end -- Union of functions
In order to satisfy the requirements of an upcomplete partial order we have to
define the supremum of a directed set of functions.
Within a directed set of functions each two functions have an upper bound
within the set. In the last chapter we have proved that two functions which
have an upper bound are consistent. This implies that all functions of a
directed set are consistent.
In order to define the supremum of a directed set we can try the following
construction.
We can map the directed set ‘d’ of functions to a set of relations.
d.image(relation)
As demonstrated in the article “Predicates as sets” each set of sets has a
supremum. Since a relation is a set of pairs a set of relations is a set of
sets. Therefore we can calculte the supremum by
d.image(relation).supremum
If we can prove that this supremum is a functional relation then we can
transform this relation back to a function by
d.image(relation).supremum.function
We have to prove two assertions
– ‘d.image(relation).supremum’ is a functional relation
– ‘d.image(relation).supremum.function’ is the supremum of ‘d’
As a first step we prove that ‘d.image(relation).supremum’ is a
functional relation.
feature -- Supremum
all(d:CURRENT?) require r1: d.is_directed local r2: rr = d.image(relation) r3: r = rr.supremum check c1: all(a:A, x,y:B) require r4: [a,x] in r r5: [a,y] in r check c2: some(s:[A,B]?) s in rr and [a,x] in s -- r3,r4 c3: some(t:[A,B]?) s in rr and [a,y] in s -- r3,r5 c4: some(f) f in d and f(a)=x -- r2,c2 c5: some(g) g in d and g(a)=y -- r2,c3 c6: all(f,g) require r6: f in d; r7: g in d r8: f(a)=x; r9: g(a)=y check c7: f.is_consistent(g) -- r1,r6,r7 ensure x=y -- c7,r8,r9 end ensure x=y end ensure d.image(relation).supremum.is_functional end
Having this assertion the following function is well defined.
supremum(d:CURRENT?): ghost CURRENT -- The supremum of a directed set of functions. require d.is_directed ensure Result = d.image(relation).supremum.function end
But we still have to prove that ‘d.supremum’ is really the supremum of ‘d’. In
order to prove this we have to demonstrate that ‘d.supremum’ is an upper bound
of ‘d’ and that all upper bounds of ‘d’ are greater equal than ‘d.supremum’.
First we verify ‘d<=d.supremum’ i.e. that ‘d.supremum’ is an upper bound of
‘d’.
all(d:CURRENT?) check c1: all(f:CURRENT) require r1: f in d local r2: r = d.image(relation).supremum check c2: f.relation<=r -- 'r' is supremum c3: f.relation.function<=r.function -- 'function' is monotonic ensure f<=d.supremum -- c3 end ensure d<=d.supremum -- c1 end
In a second step we prove that ‘d.supremum’ is the least of all upper bounds.
all(d:CURRENT?, f:CURRENT) require r1: d.is_directed r2: d<=f local r3: r = d.image(relation).supremum check c1: d.image(relation)<=f.relation -- 'relation' is monotonic c2: r<=f.relation -- c1,r3, 'r' is supremum c3: r.function<=f.relation.function -- c2, 'function' is monotonic ensure d.supremum<=f -- r3,c3 end
end -- Supremum
This completes the proof that the type A->B implements an upcomplete partial
order.
Relations are an important vehicle to write specifications. In this article we
study some properties of binary relations. First we start from binary
relations in general with domain, range, composition, images etc.
In the second part we study endorelations i.e. binary relations where the type
of the domain and the range are the same. The most important endorelations are
transitive relations. Transitive relations have a close connection to closure
systems.
All of the presented material in this article is part of the module
`predicate’ because relations are just predicates over tuples i.e. set of
tuples.
A binary relation between the type G and the type H has the type [G,H]?,
i.e. it is a set of tuples, where the first element is an object of type G and
the second element is an object of type G.
In order to express the basic functions and properties we need some generic
types as placeholders for any type.
feature E,F,G,H: ANY end
Each relation has a domain (the set of all objects which can serve as a first
component of a pair of the relation) and a range (the set of all objects which
can serve as a second component of a pair of the relation).
domain(r:[G,H]?): ghost G? -- The domain of the relation `r'. ensure Result = {x:G: some(y:H) [x,y] in r} end range(r:[G,H]?): ghost G? -- The range of the relation `r'. ensure Result = {y:H: some(x:G) [x,y] in r} end
For any relation and for each collection of relations we can define a unique
inverse.
feature inverse(r:[G,H]?): [H,G]? -- The inverse of the relation `r'. ensure Result = {y,x: [x,y] in r} end inverse(rr:[G,H]??): [H,G]?? -- The collection of all inverse relations of `rr'. ensure Result = {r: r.inverse in rr} end end
Note that [G,H]? is a set of tuples and [G,H]?? is a set of set of tuples,
i.e. a set of relations.
The function “inverse” is an involution.
inverse_involution: all(r:[G,H]?) check r.inverse.inverse = {y:H,x:G: [x,y] in r}.inverse -- def "inverse" = {x:G,y:H: [x,y] in r} -- def "inverse" = r ensure r.inverse.inverse = r end
The same applies to any collection of relations.
inverse_involution: all(rr:[G,H]??) check rr.inverse.inverse = {r:[H,G]?: r.inverse in rr}.inverse -- def "inverse" = {r:[G,H]?: r.inverse.inverse in rr} -- def "inverse" = {r:[G,H]?: r in rr} -- inverse_involution = rr ensure rr.inverse.inverse = rr end
Furthermore “inverse” is monotonic.
inverse_monotonic: all(r,s:[G,H]?) require r1: r<=s -- "<=" is the subset relation, -- i.e. `r' is contained in `s' check c1: all(x:G,y:H) require r2: [x,y] in r.inverse check c2: [y,x] in r -- r2 c3: [y,x] in s -- c2,r1 ensure [x,y] in s.inverse -- c3 end ensure r.inverse<=s.inverse end
For any collection of relations the infimum/supremum commutes with inverse
invers_commutes_limits: all(rr:[G,H]??) check c1: rr.infimum.inverse = {x:G,y:H: all(r:[G,H]?) r in rr => [x,y] in r}.inverse = {y:H,x:G: all(r:[G,H]?) r in rr => [x,y] in r} = {y:H,x:G: all(r:[H,G]?) r in rr.inverse => [y,x] in r} = rr.inverse.infimum ensure rr.infimum.inverse = rr.inverse.infimum -- c1 rr.supremum.inverse = rr.inverse.supremum -- c1 with "all,=>" replaced by "some,and" end
Since “x*y={x,y}.infimum” and “x+y={x,y}.supremum” we get the following laws.
inverse_commutes_order: all(r,s:[G,H]?) check c1: (r*s).invers = {r,s}.infimum.inverse -- def "*" = {r,s}.inverse.infimum -- inverse_commutes_limits = {r.inverse,s.inverse}.infimum -- def "inverse" on collection = r.inverse*s.inverse -- def "*" -- Note: {r,s} is a shorthand for {t:[G,H]?: t=s or t=r} ensure (r*s).inverse = r.inverse*s.inverse -- c1 (r+s).inverse = r.inverse+s.inverse -- c1 with infimum replaced -- by supremum end
We can define the composition of two binary relations.
| (r:[F,G]?, s:[G,H]?): ghost [F,H]? -- The relation `r' composed with the relation `s'. ensure Result = {x:F,z:H: some(y:G) [x,y] in r and [y,z] in s} end
The composition is associative
composition_associative: all(r:[E,F]?, s:[F,G]?, t:[G,H]?) check r|s|t = {u:E,w:G: some(v:F) [u,v] in r and [v,w] in s} | t = {u:E,x:H: some(w:G) (some(v:F) [u,v] in r and [v,w] in s) and [w,x] in t} = {u:E,x:H: some(v:F,w:G) [u,v] in r and [v,w] in s and [w,x] in t} = {u:E,x:H: some(v:F) [u,v] in r and some(w:G) [v,w] in s and [w,x] in t} = r | {v:F,x:H: some(w:G) [v,w] in s and [w,x] in t} = r|(s|t) ensure r|s|t = r|(s|t) end
The prove is simple if one remembers the basic laws
all[G](x:G, e:BOOLEAN) x in {y:G: e} = e[y:=x] all[G](e1,e2:BOOLEAN) (some(x:G) e1) and e2 = (some(x:G) e1 and e2) -- x does not occur free in e2!
The operation “inverse” distributes over composition.
inverse_distributes_composition: all(r:[F,G]?, s:[G,H]?) check (r|s).inverse = {x:F,z:H: some(y:G) [x,y] in r and [y,z] in s}.inverse = {z:H,x:F: some(y:G) [x,y] in r and [y,z] in s} = {z:H,x:F: some(y:G) [z,y] in s.inverse and [y,x] in r.inverse} = s.inverse|r.invers ensure (r|s).inverse = s.invers|r.inverse end
Unlike a function where each argument is mapped to a unique value, a relation
does not map one element of its domain to only one element of its range. It
maps each element of its domain to a set of elements of the range. We can
define a function “image” which maps each element `x’ to a set of elements. If
`x’ is not in the domain of the relation then set is empty.
In the following we define the image for an element and the image for a set of
elements and the corresponding preimage functions.
image(x:G, r:[G,H]?): ghost H? -- The image of `x' under the relation `r'. ensure Result = {y:H: [x,y] in r} end image(p:G?, r:[G,H]?): ghost H? -- The image of the set `p' under the relation `r'. ensure Result = {y:H: some(x:G) x in p and [x,y] in r} end preimage(y,H, r:[G,H]?): ghost H? -- The preimage of `y' under the relation `r'. ensure Result = y.image(r.inverse) end preimage(q:H?, r:[G,H]?): ghost H? -- The preimage of the set `q' under the relation `r'. ensure Result = q.image(r.inverse) end
The image operator is monotonic in both arguments. First we prove that
“p.image(r)” is monotonic in “p”.
image_monotonic_in_set: all(p,q:G?, r:[G,H]?) require r1: p<=q check c1: all(y:H) require r2: y in p.image(r) check c2: some(x:G) x in p and [x,y] in r -- r2 c3: all(x:G) require r3: x in p r4: [x,y] in r check c4: x in q -- r3,r1 ensure some(x:G) x in q and [x,y] in r -- c4,r4 end c5: some(x:G) x in q and [x,y] in r -- c2,c3 ensure y in q.image(r) -- c5 end ensure p.image(r) <= q.image(r) -- c1 end
Next we prove that “p.image(r)” is monotonic in “r”.
image_monotonic_in_relation: all(p:G?, r,s:[G,H]?) require r1: r<=s check c1: all(y:H) require r2: y in p.image(r) check c2: some(x:G) x in p and [x,y] in r c3: all(x:G) require r3: x in p r4: [x,y] in r check c4: [x,y] in s -- r4,r1 ensure some(x:G) x in p and [x,y] in s -- r3,c4 end c5: some(x:G) x in p and [x,y] in s -- c2,c3 ensure y in p.image(s) -- c5 end ensure p.image(r)<=p.image(s) -- c1 end
Next we can see that containment of relations can be decided by containment
of the images.
all(r,s:[G,G]?) require r1: all(x:G) x.image(r)<=x.image(s) check c1: all(x,y:G) require r2: [x,y] in r check c2: y in x.image(r) -- r2 c3. y in x.image(s) -- c2,r1 ensure [x,y] in s -- c3 end ensure r<=s -- c1 end
Similar proofs can be done for the preimage operator.
Furthermore any set `p’ in the domain of a relation `r’ mapped through the
image operator and then back through the premage operator results in a
superset of `p’.
all(r:[G,H]?, p:G?) require r1: p<=r.domain check c0: all(x:G) require r2: x in p check c1: some(y:H) [x,y] in r -- r1,r2 c2: all(y:H) require r3: [x,y] in r ensure some(z:G,y:H) z in p and [z,y] in r and [x,y] in r -- r3,r2, witness [x,y] end c3: some(z:G,y:H) z in p and [z,y] in r and [x,y] in r -- c1,c2 c4: some(y:H) y in p.image(r) and [x,y] in r -- c3 ensure x in p.image(r).preimage(r) -- c4 end ensure p<=p.image(r).preimage(r) -- c0 end
With the same reasoning it is possible to proof the dual law
all(r:[G,H]?, q:H?) require q in r.range ensure q <= q.preimage(r).image(r) -- proof similar to proof above end
We can restrict the domain or the range of a relation and get a restricted
relation.
feature <: (p:G?, r:[G,H]?): [G,H]? -- The relation `r' restricted to the domain `p' ensure Result = {x:G,y:H: [x,y] in r and x in p} end <-:(p:G?, r:[G,H]?): [G,H]? -- The relation `r' without the domain `p' ensure Result = (-p)<:r end :> (r:[G,H]?, q:H?): [G,H]? -- The relation `r' restricted to the range `q' ensure Result = {x:G,y:H: [x,y] in r and y in q} end <-:(r:[G,H]?, q:H?): [G,H]? -- The relation `r' without the range `q' ensure Result = r:>(-q) end end
The restriction and substraction of domain or range respectively are
complementary operations which split the relation into two disjoint
complementary subrelations.
feature all(r:[G,H]?, p:G?, q:H?) ensure -- proof: def `<:', def `<-:' p<:r + p<-:r = r p<:r * p<-:r = 0 r:>q + r:->q = r r:>q * r:->q = 0 end end
Some obvious laws for the domains and ranges of restricted relations.
feature all(r:[G,H]?, p:G?, q:H?) ensure -- Proofs: Just expand the definitions of "<:", ":>", -- "domain", "range", "image" and "preimage" (p<:r).domain = p (p<:r).range = p.image(r) (r:>q).range = q (r:>q).domain = q.preimage(r) end end
A subrelation `r’ of a relation `s’ remains a subrelation if we substract a
set `p’ from the domain of `s’ which is disjoint from the domain of `r’.
all(r,s:[G,H]?, p:G?) require r1: r<=s r2: p*r.domain = 0 check all(x:G,y:H) require r3: [x,y] in r check c1: x in r.domain -- r3 c2: not (x in p) -- c1,r2 c3: [x,y] in s -- r3,r1 ensure [x,y] in (p<-:s) -- c2,c3 end ensure r <= p<-:s end
The same applies to range substraction.
all(r,s:[G,H]?, q:H?) require r1: r<=s r2: q*r.range = 0 check all(x:G,y:H) require r3: [x,y] in r check c1: y in r.range -- r3 c2: not (y in q) -- c1,r2 c3: [x,y] in s -- r3,r1 ensure [x,y] in (s:->q) -- c2,c3 end ensure r <= s:->q end
A binary relation is functional if for all pairs [x,y] and [x,z] implies that
y is identical to z.
is_functional(r:[G,H]?): ghost BOOLEAN -- Is the relation `r' functional? ensure Result = all(x:G,y,z:H) [x,y] in r => [x,z] in r => y=z end
If a relation is functional, it is possible to convert the relation to a
function.
feature function(r:[G,H]?): ghost G->H require r.is_functional ensure Result = (agent(a) require a in r.domain ensure [a,Result] in r end) end end
The result of the anonymous function is defined by the property it has to
satisfy. This specification of a function is valid only if it is provable that
there exists an object which satisfies the specification and that the object
is unique. I.e. the following proof is needed.
all(r:[G,H]?, a:G) require r1: r.is_functional r2: a in r.domain ensure some(b:H) [a,b] in r -- r2 all(x,y:H) [a,x] in r => [a,y] in r => x=y -- r1, def "is_functional" end
Since the proof requires just to expand the definition of `is_functional’, it
can be done automatically by the system.
If a relation is functional then whenever two images are disjoint, the
corresponding preimages have to be disjoint as well. Here is a detailed proof
of this statement.
functional_disjoint_preimages: all(r:[G,H]?, p,q:H?) require r1: r.is_functional r2: p*q = 0 check all(x:G) require r3: x in p.preimage(r) r4: x in q.preimage(r) check c1: some(y:H) [x,y] in r and y in p -- r3 c2: some(z:H) [x,z] in r and z in q -- r4 c3: all(y:H) require r5: [x,y] in r r6: y in p check c4: all(z:H) require r7: [x,z] in r r8: z in q check c5: y = z -- r1,r5,r7 c6: y /= z -- r6,r8,r2 ensure False -- c5,c6 end ensure False -- c2,c4 end ensure False -- c1,c3 end ensure p.preimage(r)*q.preimage(r) = 0 end
There is another law for functional relations.
all(r:[G,H]?, q:H?) require r.is_functional q<=r.range ensure q = q.preimage(r).image(r) -- `<=' true in general (chapter images and preimages) -- `>=' proof see below end
In general `q<=q.preimage(r).image(r)’ is valid (see chapter about images and
preimages). It remains to be proved that `q’ is a superset in the case of a
functional relation.
all(r:[G,H]?, q:H?) require r1: r.is_functional check c1: all(y:H) require r2: y in q.preimage(r).image(r) check c2: some(x:G,z:H) z in q and [x,z] in r and [x,y] in r -- r2 c3: all(x:G,z:H) require r3: z in q r4: [x,z] in r r5: [x,y] in r check c4: y=z -- r4,r5,r1 ensure y in q -- r3,c4 end ensure y in q end ensure q.preimage(r).image(r) <= q -- c1 end
A binary relation is an endorelation if the type of the domain is the same as
the type of the range.
The most important properties of endorelations are reflexivity, transitivity
and symmetry. It is easy to define these properties formally.
is_reflexive(r:[G,G]?): ghost BOOLEAN -- Is the relation `r' reflexive? ensure Result = all(x:G) [x,x] in r end is_transitive(r:[G,G]?): ghost BOOLEAN -- Is the relation `r' transitive? ensure Result = all(x,y,z:G) [x,y] in r => [y,z] in r => [x,z] in r end is_symmetric(r:[G,G]?): ghost BOOLEAN -- Is the relation `r' symmetric? ensure Result = (r=r.inverse) end
For endorelations we have an important duality principle which states whenever
a relation is reflexive, transitive or symmetric the inverse relation satisfies
the corresponding property.
er_1: all(r:[G,G]?) ensure r.is_reflexive => r.inverse.is_reflexive r.is_transitive => r.inverse.is_transitive r.is_symmetric => r.inverse.is_symmetric end
The proofs for reflexivity and symmetry are trivial. We spell out a proof for
transitivity.
all(r:[G,G]?) require r1: r.is_transitive check c1: all(x,y,z:G) require r2: [x,y] in r.invers r3: [y,z] in r.invers check c2: [z,y] in r -- r3 c3: [y,x] in r -- r2 c4: [z,x] in r -- c2,c3,r1 ensure [x,z] in r.invers -- c4 end ensure r.invers.is_transitive -- c1 end
We claim that any restriction (domain or range) of a transitive relation
results in a transitive relation.
er_2: all(r:[G,G]?, p:G?) require r1: r.is_transitive check all(x,y,z) require r2: [x,y] in (p<:r) r3: [y,z] in (p<:r) check c1: [x,y] in r -- r2 c2: x in p -- r2 c3: [y,z] in r -- r3 c4: [x,z] in r -- c1,c3,r1 ensure [x,z] in (p<:r) -- c4,c2 end ensure (p<:r).is_transitive end
er_3: all(r:[G,G]?, q:G?) require r1: r.is_transitive check all(x,y,z) require r2: [x,y] in (r:>q) r3: [y,z] in (r:>q) check c1: [x,y] in r -- r2 c2: [y,z] in r -- r3 c3: z in q -- r3 c4: [x,z] in r -- c1,c2,r1 ensure [x,z] in (r:>q) -- c4,c3 end ensure (r:>q).is_transitive end
We call a set `p’ closed under a relation `r’ if it satisfies the following
predicate.
is_closed(p:G?, r:[G,G]?): ghost BOOLEAN -- Is the set `p' closed under the relation `r'? ensure Result = all(x,y:G) [x,y] in r => x in p => y in p end
All images of a transitive relation `r’ are closed under the relation `r’.
er_4: all(p,q:G?, r:[G,G]?) require r1: r.is_transitive r2: q = p.image(r) check c1: all(y,z:G) require r3: [y,z] in r r4: y in q check c2: some(x:G) x in p and [x,y] in r -- r4,r2 c3: all(x:G) require r5: x in p r6: [x,y] in r check c4: [x,z] in r -- r6,r3,r1 ensure z in q -- r5,c4,r2 end ensure z in q -- c2,c3 end ensure q.is_closed(r) -- c1 end
The same applies to the union of a set `p’ with its image under `r’.
er_4a: all(p,q:G?, r:[G,G]?) require r1: r.is_transitive r2: q = p.image(r) check c1: all(x,y:G) require r3: [x,y] in r r4: x in (p+q) check c2: x in p or x in q -- r4 c3: x in p => y in q -- r2 c4: x in q => y in q -- r2,r1,er_4 c5: y in q -- c2,c3,c4 ensure y in (p+q) -- c5 end ensure (p+q).is_closed(r) -- c1 end
The set of all reflexive relations, the set of all symmetric relations and the
set of all transitive relations are closure systems. Recall that a set is a
closure system if arbitrary intersections of elements remain within the set
(for more details please read the article “Complete lattices and closure
systems).
Let us state the properties first.
er_5: all(rr:[G,G]??) ensure rr={r: r.is_transitive} => rr.is_closure_system -- proof see below rr={r: r.is_reflexive} => rr.is_closure_system -- def "is_closure_system", "infimum", "is_reflexive" rr={r: r.is_symmetric} => rr.is_closure_system -- def "is_closure_system", "infimum", "is_symmetric" end
The most important closure system is the set of transitive relations. We give
a formal proof that the set of transitive relations form a closure system (the
proof for reflexive and symmetric relations is similar). The proof is spelled
out in detail in the following.
all(rr:[G,G]??) require r1: rr = {r: r.is_transitive} check c1: all(ss:[G,G]??) require r2: ss<=rr check c2: all(x,y,z:G) require r3: all(r:[G,G]?) r in ss => [x,y] in r r4: all(r:[G,G]?) r in ss => [y,z] in r check c3: all(r:[G,G]?) require r5: r in ss check c4: r.is_transitive -- r5,r2 c5: [x,y] in r -- r5,r3 c6: [y,z] in r -- r5,r4 ensure [x,z] in r -- c4,c5,c6 end ensure all(r:[G,G]?) r in ss => [x,z] in r end c7: {x,y:G: all(r:[G,G]?) r in ss => [x,y] in r}.is_transitive -- c2 c8: s.infimum.is_transitive -- c7,def "infimum" ensure ss.infimum in rr -- c8,r1 end ensure rr.is_closure_system -- c1, def "is_closure_system" end
The proof seems complicated at first glance because it contains all detailed
steps. But it is just an expansion of the definitions of “is_closure_system”,
“infimum”, and “is_transitive”. Since it requires just the expansion of
defintions and some simple algebraic transformations, the proof can be done by
the system automatically.
The boolean operation `=>’ is transitive. This can be used to create the
transitive relation `{x,y:G: e => e[x;=y]}’ over any type G with the help of a
boolean expression `e’ with one free variable.
tc_1: all(r:[G,G]?, e:BOOLEAN) require r1: r = {x,y: e => e[x:=y]} check c1: all(x,y,z:G) require r2: [x,y] in r r3: [y,z] in r check c2: e => e[x:=y] -- r2,r1 c3: e[x:=y] => e[x:=z] -- r3,r1 c4: e => e[x:=z] -- c2,c3 ensure [x,z] in r -- c4,r1 end ensure r.is_transitive -- c1 end
In the article “complete lattices and closure systems” we have given a
definition for the statement that a set `p:G?’ is closed under a relation
`r:[G,G]?’
p.is_closed(r) = all(x,y) x in p => [x,y] in r => y in p -- or equivalent p.is_closed(r) = all(x) x in p => (x.image(r)<=p)
This definition says that the relation `r’ inherits the property of an element
`x’ of being in `p’ to all elements in its image.
If we rewrite the definition of `p.is_closed(r)’ slightly we can verify the
following assertion.
tc_2: all(r,rp:[G,G]?, p:G?) require r1: rp = {x,y: x in p => y in p} check c1: (r<=rp) = all(x,y:G) [x,y] in r => x in p => y in p -- def "<=", r1 ensure rp.is_transitive -- r1, tc_1 p.is_closed(r) = (r<=rp) -- c1, def "is_closed" end
This assertions states that the relation `{x,y: x in p => y in p}’ is a
transitive relation and that the statement `p.is_closed(r)’ is equivalent to
`r’ being a subset of this relation.
From this we can conclude immediately that if `p’ is closed under a relation
`r’ then it is closed under any subset of `r’ as well.
tc_3: all(r,s:[G,G]?, p:G?) require r1: p.is_closed(r) r2: s<=r check c1: r<={x,y: x in p => y in p} -- r1, tc_2 c2: s<={x,y: x in p => y in p} -- r2,c1 ensure p.is_closed(s) -- c2, tc_2 end
This proof is straighforward, because `s’ is a subset of `r’ and the subset
relation is transitive. But we can prove the closedness under the transitive
closure of `r’ which is a superset of `r’ as well.
We claim that if a set `p’ is closed under a relation `r’ then its closed
under the transitive closure `rc’ as well.
tc_4: all(r,rc,rp:[G,G]?, p:G?) require r1: p.is_closed(r) r2: rc = r.closed(is_transitive) r3: rp = {x,y: x in p => y in p} check c1: rp.is_transitive -- r3, tc_1 c2: r <= rp -- r1, tc_2 c3: rc <= rp -- c1,c2,r2 ensure p.is_closed(rc) -- c3, tc_2 end
This proof is based on the crucial fact that the transitive closure of a
relation `r’ is the least relation which is transitive and contains `r’. Since
`rp’ is transitive and contains `r’ by `tc_2′, `rc’ must be less or equal
`rp’ which implies that `p’ is closed under the transitive closure as well.
If we have a set `p’ of elements of type G and a relation `r:[G,G]?’ then we
can ask the question which elements can be reached from `p’ via `r’ in zero or
more steps. This set can be calculated via a closure operation as demonstrated
in the paper “Complete lattices and closure systems” by the expression
`pc=p.closed(r)’.
Since `pc’ is calculated via a closure operation it is the least set which
contains `p’ and is closed under the relation `r’.
From tc_4 we can conclude doing the closure operation with `r’ and doing it
with its transitive closure must yield the same result.
tc_5: all(r,rc:[G,G], q,qc:G?) require r1: rc = r.closed(is_transitive) r2: qc = q.closed(r) check c1: all(p:G?) q<=p => q.is_closed(r) => qc<=q -- r2, closure is least set c2: qc.is_closed(r) -- r2 c3: qc.is_closed(rc) -- c2, tc_4 c4: all(p:G?) require r3: q<=p r4: q.is_closed(rc) check c5: q.is_closed(r) -- r4, r1, tc_3 ensure qc<=q -- r3,c5,c1 end c6: qc = q.closed(rc) -- c3,c4 ensure q.closed(r) = q.closed(rc) -- r2,c6 end
We want to show that the relations
r.closed(is_transitive) {x,y: y in x.image(r).closed(r)}
are equivalent. We proof this by
tc_6: all(r,s,rc:[G,G]?) require rc = r.closed(is_transitive) s = {x,y: y in x.image(r).closed(r)} check rc <= s -- lemma_1 below s <= rc -- lemma_2 below ensure rc = s end
The following `lemma_1′ proves that `s’ is transitive and includes `r’ and
therefore must be a superset of `rc’.
lemma_1: all(r,s:[G,G]?) require r1: s = {x,y: y in x.image(r).closed(r)} r2: rc = r.closed(is_transitive) check c1: all(x,y,z:G) require r2: [x,y] in s r3: [y,z] in s check c2: y in x.image(r).closed(r) -- r2 c3: z in y.image(r).closed(r) -- r3 c4: z in x.image(r).closed(r) -- c2,c3,closure is monotonic ensure [x,z] in s -- c4 end c5: s.is_transitive -- c1 c6: all(x,y) require r4: [x,y] in r check c7: y in x.image(r) -- r4 c8: y in x.image(r).closed(r) -- c7, closure is ascending ensure [x,y] in s -- c8 end c9: r<=s -- c6 ensure rc <= s -- c5,c9,r2 end
The following `lemma_2′ shows that a transitive closure `rc’ of a relation `r’
always produces closed images (under the relation `r’) and every image under
`rc’ contains the corresponding image under `r’ and therfore must contain the
corresponding images of `s’ which are least sets by definition.
lemma_2: all(r,s,rc:[G,G]?) require r1: rc = r.closed(is_transitive) r2: s = {x,y: y in x.image(r).closed(r)} check c0: all(x:G) check c1: r<=rc -- r1 c2: x.image(r) <= x.image(rc) -- c1, image monotonic c3: x.image(rc).is_closed(rc) -- r1, er_4 c4: x.image(rc).is_closed(r) -- c3, tc_5 c5: x.image(r).closed(r) <= x.image(rc) -- c2,c4 ensure x.image(s) <= x.image(rc) -- c5,r2 end ensure s <= rc -- c0 end
We want to show that the sets
p.closed(r) p + p.image(r.closed(is_transitive))
are equivalent. Proof:
tc_7: all(p,pc:G?, r,rc:[G,G]?) require pc = p.closed(r) rc = r.closed(is_transitive) check pc <= p + p.image(rc) -- lemma_1 below p + p.image(rc) <= pc -- lemma_2 below ensure pc = p + p.image(rc) end
lemma_1: all(p,pc:G?, r,rc:[G,G]?) require r1: pc = p.closed(r) r2: rc = r.closed(is_transitive) check c1: p <= p + p.image(rc) c2: p.image(rc).is_closed(r) -- r2, tc_4 c3: all(x,y:G) require r3: x in p r4: [x,y] in r check c4: y in x.image(r) -- r4 c5: y in x.image(rc) -- c4,r2 ensure y in p+p.image(rc) end c4: (p+p.image(rc)).is_closed(r) -- c3 ensure pc <= p + p.image(rc) -- c1,c4,r1 end
lemma_2: all(p,pc:G?, r,rc:[G,G]?) require r1: pc = p.closed(r) r2: rc = r.closed(is_transitive) check c3: p.image(r) <= pc -- r1 c4: p.image(r).closed(r) <= pc.closed(r) -- c3, closure monotonic c5: p.image(r).closed(r) <= pc -- c4, closure idempotent c6: p.image(rc) = p.image({x,y: y in x.image(r).closed(r)}) -- tc_6 c7: p.image(rc) = p.image(r).closed(r) -- c6 c8: p.image(rc) <= pc -- c5,c7 ensure p + p.image(rc) <= pc -- c1,c8 end
Complete lattices are important because of their connection to closure
systems. Closure systems arise in many cases.
Some examples:
1. Graphs: If we have a graph we often use the phrase “All the nodes reachable
from a specific node”. A set of nodes of a graph which contains all reachable
nodes is a closed set and the collection of all such sets is a closure system.
2. Grammars: If we define a formal language e.g. via a context free grammar we
define a set of terminals, a set of nonterminals, a start symbol and a set of
production rules. We define a sentential form to be a string of grammar
symbols which can be produced from the start symbol by using zero or more
production rules. The language of a grammar are all the string of terminal
symbols which can be produced from the start symbol. It turns out that a set
of strings of grammar symbols which contain all producible strings from any of
the strings within the set is a closed set and the collection of all such sets
is a closure system.
3. Transitive closures: For any relation we can define a transitive and a
reflexive transitive closure. The set of all transitive (or reflexive
transitive) relations are closure systems as well. We often use the wording:
“We have a relation r and define the relation rtrans to be the least
transitive relation which contains r”.
Many more examples can be found. But these three should be sufficient to see
the importance of closure system.
Closure systems are best studied within the abstract setting of a complete
lattice. A complete lattice is a minimal structure with sufficient properties
to define the basic structures of closure systems and give a lot of proofs.
A complete lattice is a lattice where each set of elements has an infimum and
a supremum.
deferred class COMPLETE_LATTICE inherit LATTICE end
feature -- Lattice operations and axioms * (a,b:CURRENT): CURRENT deferred end + (a,b:CURRENT): CURRENT deferred end <= (a,b:CURRENT): BOOLEAN ensure Result = (a=a*b) end = (a,b:CURRENT): BOOLEAN deferred end all(a,b,c:CURRENT) deferred ensure a=b => a~b a*b = b*a a+b = b+a a*b*c = a*(b*c) a+b+c = a+(b+c) a = a*(a+b) a = a+(a*b) end end
feature -- Definitions for set of elements <= (a:CURRENT, p:CURRENT?): ghost BOOLEAN ensure Result = all(x) x in p => a<=x end <= (p:CURRENT?, a:CURRENT): ghost BOOLEAN ensure Result = all(x) x in p => x<=a end low_set(p:CURRENT?): ghost CURRENT? ensure Result = {x: x<=p} end up_set(p:CURRENT?): ghost CURRENT? ensure Result = {x: p<=x} end is_minimum(a:CURRENT, p:CURRENT?): ghost BOOLEAN ensure Result = (a in p and a<=p) end is_maximum(a:CURRENT, p:CURRENT?): ghost BOOLEAN ensure Result = (a in p and p<=a) end is_infimum(a:CURRENT, p:CURRENT?): ghost BOOLEAN ensure Result = a.is_maximum(p.low_set) end is_supremum(a:CURRENT, p:CURRENT?): ghost BOOLEAN ensure Result = a.is_minimum(p.up_set) end end
feature -- Supremum and infimum infimum(p:CURRENT?): CURRENT deferred end supremum(p:CURRENT?): CURRENT deferred end 0: CURRENT ensure Result = (0:CURRENT?).supremum end 1: CURRENT ensure Result = (0:CURRENT?).infimum end all(p:CURRENT?) deferred ensure p.infimum.is_infimum(p) p.supremum.is_supremum(p) end end
A closure system is a set of elements of the complete lattice which is closed
under arbitrary meet operations. I.e. the meet “a*b” of any two elements “a”
and “b” of the closure system is in the closure system and also the infimum of
each subset of the closure system (a meet of arbitrary elements of the closure
system) is an element of the closure system. Since the second condition
(arbitrary meets) implies the first one (any meets of two elements) we use the
stronger to define a closure system.
feature is_closure_system(p:CURRENT?): ghost BOOLEAN -- Is the set `p' a closure system, i.e. is `p' closed under -- arbitrary meet operations? ensure Result = all(q:CURRENT?) q<=p => q.infimum in p end end
Being a closure system is a rather abstract property of a set. In specific
closure systems the elements of a closure system can be sets which are closed
with respect to a certain property, i.e. a closure system can be a set of
sets. But within the context of the module “complete_lattice” a closure system
is just a set of elements. Even in this abstract setting we can prove some
important facts about closure systems.
A closure system must contain the top element.
all(p:CURRENT?) require r1: p.is_closure_system check c1: 0:CURRENT? <= p -- the empty set is a subset of any set c2: (0:CURRENT?).infimum in p -- c1, def "is_closure_system" ensure 1 in p -- def "1", c2 end
In the following we define a closure operation and show that a closure
operation maps any element “a” to the least element of a closure system which
is above “a”.
For any element “a” we can define the set {x:a<=x} to represent the set of all
elements above “a”. Let “p” be a closure system. Then “p*{x:a<=x}” is the set
of all elements of “p” which are above “a” (the “*” operator on sets is
intersection). This set is clearly a subset of “p”. Therefore the infimum of
this set is in “p”. We define the closure operation as
feature closed(a:CURRENT, p:CURRENT?): ghost CURRENT -- The closure of `a' with respect to the set `p'. ensure Result = (p * {x:a<=x}).infimum end end
The function “closed” can be applied to any set “p”. In general “closed” is
ascending and monotonic in its first argument. If the set “p” is a closure
system the function is idempotent as well.
First we prove that “closed” is ascending in its first argument, i.e. it
always selects a greater equal element.
feature closed_ascending: all(a:CURRENT, p:CURRENT?) check c1: a = {x:a<=x}.infimum -- general law c2: all(p,q:CURRENT?) p<=q => q.infimum<=p.infimum -- general law: infimum reverses order c3: {x:a<=x}.infimum <= (p*{x:a<=x}).infimum -- c2 c4: a <= (p*{x:a<=x}).infimum -- c1, c3 ensure a <= a.closed(p) -- c4, def "closed" end end
Next we prove that “closed” is monotonic in its first argument, i.e. a
“greater” element gets a “greater” closure.
feature closed_monotonic: all(a,b:CURRENT, p:CURRENT?) require r1: a<=b check c1: {x:b<=x} <= {x:a<=x} -- r1, transitivity "<=" c2: all(p,q,r:CURRENT?) q<=r => p*q<=p*r -- general law c3: p*{x:b<=x} <= p*{x:a<=x} -- c1,c2 c4: (p*{x:a<=x}).infimum <= (p*{x:b<=x}).infimum -- c3, infimum reverses order ensure a.closed(p) <= b.closed(p) -- c4, def "closed" end end
For more properties of “closed” we need to exploit the fact that the second
argument is a closure system. We can prove that the function “closed” always
selects one element of a closure system.
feature closed_selects: all(a:CURRENT, p:CURRENT?) require r1: p.is_closure_system check c1: p*{x:a<=x} <= p -- trivial c2: (p*{x:a<=x}).infimum in p -- c1, r1, def "is_closure_system" ensure a.closed(p) in p end end
Next we see that any element of a closure system is mapped to itself.
feature closed_maps_p2p: all(a:CURRENT, p:CURRENT?) require r1: p.is_closure_system r2: a in p check c1: a <= a.closed(p) -- closed_ascending c2: a in {x:a<=x} -- trivial c3: a in p*{x:a<=x} -- r2,c2 c4: (p*{x:a<=x}).infimum <= a -- c3, infimum in low_set c5: a.closed(p) <= a -- c4, def "closed" ensure a.closed(p) = a -- c1,c5 end end
The last two assertions are sufficient to show that the closure operation is
idempotent.
feature closed_idempotent: all(a:CURRENT, p:CURRENT?) require r1: p.is_closure_system check c1: a.closed(p) in p -- r1, closed_selects ensure a.closed(p).closed(p) = a.closed(p) -- c1, closed_maps_p2p end end
In the last step we can convince ourselves that “a.closed(p)” selects the least
element of the closure system “p” which is above “a”.
feature closed_selects_least: all(a,b:CURRENT, p:CURRENT?) require r1: p.is_closure_system check c1: a.closed(p) in {x:a<=x} -- closed_ascending c2: a.closed(p) in p -- closed_selects c3: a.closed(p) in p*{x:a<=x} -- c1,c2 c4: all(b) require r2: b in p r3: b in {x:a<=x} check c5: b.closed(p)=b -- closed_maps_p2p c6: a<=b -- r3 c7: a.closed(p)<=b.closed(p) -- closed_monotonic ensure a.closed(p) <= b -- c5,c7 end ensure a.closed(p).is_minimum(p*{x:a<=x}) -- c3,c4 end end
In the previous chapter we defined a closure operation via a closure
system. The reverse is possible and equally powerful. We can define properties
for a closure map and can define a closure system as the range of a closure
map.
feature is_closure_map(f:CURRENT->CURRENT): ghost BOOLEAN ensure Result = ( f.is_total and f.is_ascending and f.is_monotonic and f.is_idempotent ) end end
The function “x->x.closed(p)” is a closure map provided that “p” is a closure
system. It is total in its first argument, it is ascending, monotonic and
idempotent as proved in the previous chapter.
Now we want to prove that any function which satisfies the above properties
defines uniquely a closure system.
feature closure_system_via_map: all(f:CURRENT->CURRENT) require r1: f.is_closure_map check c0: all(p:CURRENT?) require r2: p <= f.range check c1: p.infimum <= f[p.infimum] -- r1, f.is_ascending c2: p.image(f) = p -- r2, f.is_idempotent c3: all(x) require r3: x<=p check c4: f[x]<=p.image(f) -- f.is_monotonic ensure f[x]<=p -- c4,c2 end c5: f[p.infimum]<=p -- c3, infimum in low_set c6: f[p.infimum]<=p.infimum -- c5, p.infimum is maximum -- of low_set c7: f[p.infimum] = p.infimum -- c1,c6 c8: some(x) f[x] = p.infimum -- c7, witness p.infimum ensure p.infimum in f.range -- c8 end ensure f.range.is_closure_system -- c0, def "is_closure_system" end end
The concept of a closure system and a closure map is equivalent. Each closure
system defines a closure map via the function “closed” and each closure map
defines a closure system by its range.
A closure map is a total function which is ascending, monotonic and
idempotent. The range of a closure map is a closure system.
A total function which is ascending and monotonic is not yet a closure map. But
we can construct a closure system from a total ascending and monotonic
function.
We are within the context of a complete lattice. Therefore any total ascending
function “f” must have fixpoints. A complete lattice has a top element “1” and
since the top element is the maximum of all elements it has to be a fixpoint
of f, i.e. f[1]=1. We can prove that the set of all fixpoints “f.fixpoints” is
a closure system. In order to prove this we have to show that the infimum of
any subset of fixpoints is again a fixpoint.
feature fixpoint_lemma: all(f:CURRENT->CURRENT, p:CURRENT?) require r1: f.is_total r2: f.is_ascending r3: f.is_monotonic r4: p = f.fixpoints check c0: all(q:CURRENT?) require r5: q<=p check c1: q.image(f) = q -- r4,r5 c2: q.infimum <= q -- def "infimum" c3: f[q.infimum] <= q.image(f) -- c2,r3 c4: f[q.infimum] <= q -- c1,c3 c5: f[q.infimum] <= q.infimum -- c4, def "infimum" c6: q.infimum <= f[q.infimum] -- r2 c7: f[q.infimum] = q.infimum -- c5,c6 ensure q.infimum in p -- c7 end ensure p.is_closure_system -- c0 end end
With this we a have third method to get a closure of an arbitrary element “a”
of a complete lattice. If we have a total, ascending and monotonic function
“f”, then
a.closed(f.fixpoints)
returns the least fixpoint with is greater equal “a”. The already proved
assertion “closed_selects_least” and the fact that “f.fixpoints” is a closure
system for a total, ascending and monotonic function guarantees this.
feature all(a:CURRENT, f:CURRENT->CURRENT) require r1: f.is_total r2: f.is_ascending r3: f.is_monotonic check c1: f.fixpoints.is_closure_system -- fixpoint_lemma ensure a.closed(f.fixpoints).is_minimum(f.fixpoints*{x:a<=x}) -- c1, closed_selects_least end end -- Note: f.fixpoints*{x:a<=x} is the set of fixpoints of f above a.
The type PREDICATE[G] is a complete lattice. Each predicate defines a set. And
each set of sets has an infimum (the intersection of all sets) and an supremum
(the union of all sets).
-- module: predicate immutable class PREDICATE[G] inherit BOOLEAN_LATTICE redefine <= end COMPLETE_LATTICE redefine <=, 0, 1 end end feature -- all usual lattice and boolean lattice features ... -- see article "Predicates as sets" end feature -- Infimum and supremum infimum(pp:CURRENT?): CURRENT -- The intersection of all sets in `pp'. ensure Result = {x:G: all(p) p in pp => x in p} end supremum(pp:CURRENT?): CURRENT -- The union of all sets in `pp'. ensure Result = {x:G: some(p) p in pp and x in p} end end
In this framework it is possible to define closed sets.
The type [G,G]? is a shorthand for PREDICATE[G,G] which is a shorthand for
PREDICATE[ [G,G] ] i.e. the set of tuples [G,G]. I.e. [G,G]? is an
endorelation over G.
If we have a set s:G? and a relation r:[G,G]? we sometimes want to define the
set which contains “s” and all elements which are reachable from “s” via the
relation “r”.
Some functions on relations.
feature -- Functions of relations domain(r:[G,G]?): ghost G? -- The domain of the relation `r'. ensure Result = {x:G: some(y:G) [x,y] in r} end range(r:[G,G]?): ghost G? -- The range of the relation `r'. ensure Result = {x:G: some(y:G) [x,y] in r} end image(p:G?, r:[G,G]?): ghost G? -- The image of the set `p' under the relation `r' ensure Result = {y:G: some(x:G) x in p and [x,y] in r} end preimage(p:G?, r:[G,G]?): ghost G? -- The preimage of the set `p' under the relation `r' ensure Result = {x:G: some(y:G) y in p and [x,y] in r} end all(r:[G,G]?, p:G?) ensure p.image(r) <= r.range -- def "image", "range" p.preimage(r) <= r.domain -- def "preimage", "domain" end end
The functions “p:G?->p.image(r)” and “p:G?->p.preimage(r)” are monotonic. We
give a proof for the first one. The proof for the second is similar.
feature all(r:[G,G]?) check c1: all(p,q:G?) require r1: p<=q check c2: all(b:G) require r2: b in p.image(r) check c3: some(a:G) a in p and [a,b] in r c4: all(a:G) require r3: a in p r4: [a,b] in r check c5: a in q -- r3,r1 c6: some(a:G) a in q and [a,b] in r -- c5, r4 ensure b in q.image(r) end ensure b in q.image(r) -- c3,c4 end ensure p.image(r) <= q.image(r) -- c2 end ensure (p:G? -> p.image(r)).is_monotonic -- c1 end end
We can use a relation to define a closure system.
feature is_closed(p:G?, r:[G,G]?): ghost BOOLEAN -- Is the set `p' closed with respect to the relation `r'? ensure Result = all(a,b:G) a in p => [a,b] in r => b in p end closure_system(r:[G,G]?): ghost G?? -- The closure system generated from the relation `r'. ensure Result = {p:G?: p.is_closed(r)} end
The predicate “p.is_closed(r)” says that whenever there is an element “a” in
“p” and there is an element “b” such that “[a,b] in r”, then “b” is in “p” as
well. We call such a set “p” a set closed with respect to the relation “r”.
It is easy to verify that “p.is_closed(r)” is equivalent “p.image(r)<=p”
all(p:G?, r:[G,G]?) require r1: p.image(r) <= p check c0: all(a,b:G) require r2: a in p r3: [a,b] in r check c1: some(x:G) x in p and [x,b] in r -- r2,r3 c2: b in p.image(r) -- c1, def "image" ensure b in p -- c2,r1 end ensure p.is_closed(r) -- c0 end all(p:G?, r:[G,G]?) require r1: p.is_closed(r) check c0: all(y:G) require r2: y in p.image(r) check c1: some(x:G) x in p and [x,y] in r -- r2 c2: all(x:G) x in p and [x,y] in r => y in p -- r1 ensure y in p -- c1,c2 end ensure p.image(r) <= p -- c0 end end
We define for any relation “r” the function “p:G?->p+p.image(r)”. This
function adds to each set “p” the set of all elements which can be reached in
one step using the relation “r”. If we can add nothing more, i.e. “p.image(r)”
is already a subset of “p”, then we have reached a fixpoint of the function.
It is evident that this function is total and ascending. The monotonicity can
be verified by using the monotonicity of “image”.
feature all(r:[G,G]?, f:G?->G?) require r1: f = (p->p+p.image(r)) check c1: all(p,q:G?) require r2: p<=q check c2: p.image(r) <= q.image(r) -- "image" is monotonic ensure f[p]<=f[q] -- r2,c2 end ensure f.is_total -- evident from definition f.is_ascending -- evident from definition f.is_monotonic -- c1 end end
The fixpoints of “p->p+p.image(r)” form a closure system which is the same as
the result of the above define function “r.closure_system”.
feature all(r:[G,G]?, f:G?->G?) require r1: f = (p->p+p.image(r)) check c1: f.fixpoints = {p:G?: p.image(r)<=p} -- evident c2: {p:G?: p.image(r)<=p} = {p:G?, p.is_closed(r)} -- see above proof that p.image(r)<=p and p.is_closed(r) -- are equivalent c3: {p:G?: p.is_closed(r)} = r.closure_system -- def "closure_system" ensure f.fixpoints = r.closure_system -- c1,c2,c3 end end
Since any relation defines uniquely a closure system, we can close any set of
elements (even singleton sets) with respect to any relation using the
following functions.
feature closed(p:G?, r:[G,G]?): ghost G? -- The set `p' closed with respect to the relation `r'. ensure Result = p.closed(r.closure_system) end closed(a:G, r:[G,G]?): ghost G? -- The set {a} closed with respect to the relation `r'. ensure Result = {a}.closed(r) -- {a} is a shorthand for {x:G: x~a} end end
Note that each of these functions returns the least set which is closed under
the relation “r”.
Since functions are relations we can use functions as well to close sets. A
function f:G->G defines the relation
feature relation(f:G->G): ghost [G,G]? -- The relation defined by the function `f'. ensure Result = {a,b: a in f.domain and b~f[a]} end closed(p:G?, f:G->G): ghost G? -- The set `p' closed under the function `f'. ensure Result = p.closed(f.relation) end closed(a:G, f:G->G): ghost G? -- The set {a} closed under the function `f'. ensure Result = {a}.closed(f.relation) end end
Sets abound in mathematics. If a mathematician wants to treat a collection of
objects which satisfy a certain property as an entity, he defines a set.
Sets are a very powerful tool in specifications. In order to obtain good
readability we introduce a set like notation.
The expression
{1,2,3}
is the set of the three numbers “1”, “2” and “3”. But in general the elements
of a set are expressed by a boolean expression. The expression
{x:NATURAL: x.is_even}
is the set of all natural numbers which are even. This is the basic notation
for mathematical sets in our programming language.
{x:T: bexp} -- set of all x which satisfy the boolean expression `bexp'
The variable “x” is a bound variable. Its name is not relevant and can be
changed arbitrarily, i.e. we have
{x:T: bexp} = {y:T: bexp[x:=y]} -- "y" must not occur free in `bexp' -- bexp[x:=y]: all free occurrences of `x' within `bexp' substituted by `y'
In many cases the type T of the bound variable can be inferred from the
context. In these case the expression
{x: bexp}
is sufficient to describe the set.
The notation “{a,b,…}” is just a shorthand.
-- shorthand {a,b,...} = {x:T: x~a or x~b or ...}
If we have a set we can ask whether an element belongs to the set
a in {x:T: bexp} -- Does `a' belong to the set "{x:T: bexp}"?
For this boolean expression the following equivalence is evident
a in {x:T: bexp} = bexp[x:=a] -- bexp[x:=a]: all free occurrences of `x' within `bexp' substituted by `a'
The set of all elements of a certain type is
{x:T: True}
and the empty set is
{x:T: False}
We can build the union (“+”) and the intersection (“*”) of sets and the
complement (“-“) of sets with the following notations
{x:T: e1} + {x:T: e2} = {x:T: e1 or e2} -- set union {x:T: e1} * {x:T: e2} = {x:T: e1 and e2} -- set intersection -{x:T: e} = {x:T: not e} -- set complement
The relation “<=” is the subset relation with the following notation
{x:T: e1} <= {x:T: e2} -- Is the first set contained in the second? -- The "<=" operator is defined as ({x:T: e1} <= {x:T: e2}) = (all(x:T) e1 => e2) -- ^ boolean implication -- ^ boolean equality -- ^ subset relation
Two sets are equal if they contain the same elements
({x:T: e1} = {x:T: e2}) = (all(x:T) e1 = e2) -- ^ boolean equality -- ^ boolean equality -- ^ set equality
Clearly for sets defined with arbitrary boolean expressions the subset
operator “<=” and the set equality operator “=” are not decidable in
general. Therefore they must be represented by ghost functions.
The set expression “{x:T: bexp}” has the type PREDICATE[T]. Since predicates
are used frequently there is the shorthand T? for PREDICATE[T].
The type PREDICATE[T] is a builin type with the following basic definitions
-- module: "predicate" immutable class PREDICATE[G] end feature -- Basic functions in (e:G, p:G?): BOOLEAN -- Is the element `e' contained in the set `p'? note built_in end <= (a,b:G?): ghost BOOLEAN -- Is `a' a subset of `b'? ensure Result = all(x:G) x in a => x in b end = (a,b:G?): ghost BOOLEAN -- Do the sets `a' and `b' have the same elements? ensure Result = (a<=b and b<=a) end * (a,b:G?): G? -- The intersection of the sets `a' and `b'. ensure Result = {x: x in a and x in b} end + (a,b:G?): G? -- The union of the sets `a' and `b'. ensure Result = {x: x in a or x in b} end - (a:G?): G? -- The complement of the set `a'. ensure Result = {x: not (x in a)} end 0: G? -- The empty set ensure Result = {x: False} end 1: G? -- The universal set ensure Result = {x: True} end end
The subset relation is reflexive and antisymmetric
feature all(a,b:G?) ensure reflexive: a<=a -- def "<=" antisymmetric: a<=b => b<=a => a=b -- def "<=", "=" end -- Note: - Implication "=>" is right associative -- - "p=>q=>r" is equivalent to "p and q => r" end
The transitivity of the subset relation can be verified with the following
detailed proof.
feature transitive: all(a,b,c:G?) require r1: a<=b r2: b<=c check c1: all(x:G) require r3: x in a check c2: x in b -- r3, r1, def "<=" ensure x in c -- c2, r2, def "<=" end ensure a<=c -- c1, def "<=" end end
The reflexivity, symmetry and transitivity of the equality relation is an
immediate consequence of the definition and the corresponding properties of the
subset relation.
feature all(a,b,c:G?) ensure reflexive: a=a symmetric: a=b => b=a transitive: a=b => b=c => a=c end end
The intersection of two sets “a*b” is contained in both sets, therefore it has
to be a subset of both sets. If we build the intersection of a set “b” with
one of its subsets “a” then the intersection must be equal to the subset. The
fact that “a<=b” is equivalent to “a=a*b” is evident. Here is a pedantic proof
of this fact.
all(a,b:G?) check c1: require r1: a<=b check c2: all(x) x in a => x in b -- r1, def "<=" c3: all(x) require r2: x in a ensure x in a and x in b -- r2, c2 end c4: all(x) require r3: x in a and x in b check ensure x in a -- r3 end ensure a=a*b -- c3, c4, def "=" end c5: require r4: a=a*b check c6: all(x) x in a => x in a*b -- r4, def "=" c7: all(x) x in a => x in b require r5: x in a check c8: x in a*b -- r5, c6 c9: x in a and x in b -- c8, def "*" ensure x in b -- c9 end ensure a<=b end ensure a<=b = (a=a*b) -- c1, c5 end
Note that this proof just requires the expansion of function definitions and
the application of some very simple laws of logic. Therefore the proof engine
can do this proof automatically.
With a similar technique it can be shown that the commutative, associative and
absorption laws for “*” and “+” are valid. The reader is encouraged to do the
detailed proofs. Here just the assertions are stated with a hint on how to
construct the proof.
all(a,b:G?) ensure com_1: a*b = b*a -- def "=", "*" com_2: a+b = b+a -- def "=", "+" assoc_1: a*b*c = a*(b*c) -- def "=", "*" assoc_2: a+b+c = a+(b+c) -- def "=", "+" absorb_1: a = a*(a+b) -- def "=", "*", "+" absorb_2: a = a+(a*b) -- def "=", "*", "+" dist_1: a*(b+c) = a*b+a*c -- def "=", "*", "+" dist_2: a+(b*c) = (a+b)*(a+c) -- def "=", "*", "+" end
The empty set is the neutral element of set union and the universal set is the
neutral element for set intersection. The intersection of a set with its
complement yields the empty set and the union of a set with its complement
yields the universal set. This is expressed in the following assertions:
all(a:G?) ensure -- proof: expansion of function definitions and elementary laws -- of logic neutral_0: a+0 = a neutral_1: a*1 = a compl_0: a*(-a) = 0 compl_1: a+(-a) = 1 end
In the last chapters some elementary laws have been shown which are satisfied
by predicates. Exactly these laws are the axioms of a boolean lattice (see
article about boolean lattices). From this we can conclude that predicates are
boolean lattices, i.e. the class PREDICATE can inherit from BOOLEAN_LATTICE
and inherit all the additional properties of a boolean lattice (e.g. order
reversal, de Morgan’s laws, double negation).
immutable class PREDICATE inherit ghost BOOLEAN_LATTICE redefine <= end end
Two remarks are important:
– The class PREDICATE must inherit BOOLEAN_LATTICE as a ghost parent. This is
necessary because PREDICATE defines the relations “<=” and “=” as ghost
functions. In the BOOLEAN_LATTICE these relations are defined as normal
relations. By inheriting the parent as “ghost” the descendant has the
freedom to define the functions as ghost functions or normal functions.
– The class BOOLEAN_LATTICE has a definition of “<=”. The class PREDICATE
overrides this definition. This has to be indicated in the inheritance
clause. The definition of “<=” from BOOLEAN_LATTICE becomes a proof
obligation within the class PREDICATE. In the previous chapters the
equivalence of “a<=b” and “a=a*b” have been proved.
We can build set of sets i.e. expressions of type T??.
The set “ss1” defined as
ss1 = {s:NATURAL?: all(n,m:NATURAL) n in s => m in s => n+m in s}
describes the set of all set of natural numbers which are closed with respect
to addition. Some examples of sets which belong to the specified set of sets:
s0 = {} s1 = {0,2,4,...} -- Warning: Ellipsis ... is not part of the s2 = {0,3,6,...} -- programming language s3 = {0,2,3,4,6,8,9,10,12,14,15,...} s4 = {2,3,4,6,8,9,10,12,14,15,...} s5 = {2,3,4,6,8,9,10,11,12,14,15,...} -- s4 plus multiples of "11" ... sn = {0,1,2,3,...} s0 in ss1, s1 in ss1, ...
If we have a set of sets like “ss1” we can ask the questions “What is the
intersection of all sets of “ss1?”. Note that “ss1” contains an infinite number
of sets, therefore such a function cannot be obtained by combining the
intersection operator “*”. From the above definition it is clear that the
empty set is the intersection of all sets of “ss1”.
In order to make the question a little bit more interesting we construct
another set of sets
ss2 = {s:NATURAL?: 2 in s and 3 in s}
and ask the question “What is the intersection of all sets contained in
“ss1*ss2 (i.e. the set of sets which satisfy both conditions)?”. The set
“ss1*ss2” has the two members “s3” and “s4” and a lot of supersets of these
two sets. The intersection of all these sets is “s4”.
From this example one might conclude that the intersection of a set of sets is
always a member of the set of sets. But this is not true. For a simple
counterexample consider the set of sets
ss3 = {s:NATURAL?: 2 in s or 3 in s}
and build the intersection of all sets in “ss1*ss3”. The set “ss1*ss3” has the
following members
{0,2,4,...} {2,4,6,...} {0,3,6,...} {3,6,9,...} -- and many supersets of these sets
The intersection of all these sets is the empty set which is not contained in
“ss1*ss3” because it fails the specification to contain either “2” or “3”.
We can define a function “infimum” to return the intersection of a set of sets
and a function “supremum” to return the union of a set of sets.
feature infimum(pp:G??): ghost G? -- The intersection of all sets contained -- in the set of sets `pp'. ensure Result = {x: all(p) p in pp => x in p} end supremum(pp:G??): ghost G? -- The union of all sets contained -- in the set of sets `pp'. ensure Result = {x: some(p) p in pp and x in p} end end
The set “pp.infimum” contains only the elements which are contained in all
sets in “pp”, i.e. “pp.infimum” is the intersection of all sets in “pp”. The
set “pp.supremum” contains only elements which are contained in at least one
set of “pp”, i.e. “pp.supremum” is the union of all sets in “pp”.
But is “pp.infimum” really an infimum and “pp.supremum” supremum? This
question is nontrivial because the notions “infimum” and “supremum” are
defined independently of “intersection” and “union”.
Let us recall the definition of an infimum. The infimum of a set “pp” (here
set of sets) is the maximum of the lower set of “pp”. The lower set of “pp” is
the set of all sets which are subsets of all sets in “pp”
pp.low_set = {p: all(q) q in pp => p<=q}
In order to prove that “pp.infimum” is an infimum we have to prove
all(pp:G??) ensure contained: all(p) p in pp => pp.infimum<=p maximum: all(q) (all(p) p in pp => q<=p) => q<=pp.infimum end
A detailed proof of “contained”:
contained: all(pp:G??, p:G?) require r1: p in pp check c1: all(x:G?) require r2: all(r) r in pp => x in r ensure x in p -- r1, r2 end c2: {x: all(r) r in pp => x in r} <= p -- def "<=", c1 ensure pp.infimum<=p -- c2, def "infimum" end
A detailed proof of “maximum”:
all(pp:G??, q:G?) require r1: all(p) p in pp => q<=p check c1: all(x:G, r:G?) require r2: x in q r3: r in pp check c2: all(p) p in pp => x in p -- r2, r1 ensure x in r -- r3, c2 end c3: q <= {x: all(r) r in pp => x in r} -- c1 ensure q<=pp.infimum -- c3, def "infimum" end
The proof that “pp.supremum” is the minimum of “pp.up_set” is a similar
excercise.
The infimum of the empty set is the universal set and the supremum of the
empty set is the empty set. I.e. we have
{p:G?: False}.infimum = 1 {p:G?: False}.supremum = 0
These claims are proved by simple expansion of the definitions of “infimum” and
supremum” and elementary laws of logic.
A complete lattice is a lattice where each set of elements has an infimum and
a supremum. In the previous chapter we have defined the two functions
“infimum” and “supremum” and have shown that the return value of these
functions satisfies the specification of an “infimum” and a “supremum”
respectively. Therefore the class PREDICATE can inherit not only from the
class BOOLEAN_LATTICE but also from the class “COMPLETE_LATTICE” because it
satisfies the axioms of both. I.e. we can define the class PREDICATE as
immutable class PREDICATE[G] inherit ghost BOOLEAN_LATTICE redefine <= end ghost COMPLETE_LATTICE redefine <= 0 1 end end
The class PREDICATE overrides the definitions of “<=”, “0” and “1” of its
parent COMPLETE_LATTICE because it has its own definition of these
functions. But we have demostrated that the class PREDICATE satisfies the
definitions of these inherited functions.
With this declaration all assertions of the module “boolean_lattice” and
“complete_lattice” are inherited as valid assertions.
This article is dedicated to boolean lattices. A boolean lattice is another
name for a boolean algebra.
A boolean lattice is an algebraic structure with two binary operators “*” and
“+” which represent “and” and “or”, a unary operator “-” which represents
negation. The binary operators are commutative, associative and
distributive. Furthermore there are the constants “0” and “1” which represent
“False” and “True”. “0” is neutral for “+” and “1” is neutral for “*”. In
addition to that we have the axioms “a*(-a)=0” and “a+(-a)=1” for all “a”.
I.e. a boolean lattice is a distributive lattice with some additional
operations, some additional axioms and some lost axioms (e.g. the absorption
laws are not axioms in a boolean lattice).
The fact that in a boolean lattice some of the inherited axioms loose their
axiom status requires some attention.
Abstract modules can have axioms and abstract operations. Axioms are
assertions which do not have a proof. They are assumptions. From the axioms
some other assertions can be derived. I.e. we have the following logic within
an abstract module “m1”.
m1_axioms => m1_assertions
If we design a module “m2” which inherits “m1” we can decide which of the
axioms we keep and which of them we abandon. I.e. we split the axioms
m1_axioms = kept_axioms + lost_axioms
Furthermore we can give “m2” some new axioms, i.e. we get
m2_axioms = kept_axioms + new_axioms
The axioms of “m2” are not sufficient to prove the derived assertions of
“m1” because some axioms of “m1” lost their axiom status. In order to use the
asssertions of “m1” the module “m2” has to provide a proof for all abandoned
axioms based on its own axioms.
m2_axioms => lost_axioms
These proofs must be done without the use of the derived assertions of
“m1”. Otherwise we introduce circular reasoning. As soon as the abandoned
axioms are proved within “m2” the full power of the derived assertions of the
module “m1” is available within the module “m2”.
A boolean lattice inherits from a distributive lattice. A distributive lattice
is based in the following operations and axioms
deferred class DISTRIBUTIVE_LATTICE end feature -- Operations * (a,b: CURRENT): CURRENT deferred end + (a,b: CURRENT): CURRENT deferred end = (a,b: CURRENT): BOOLEAN deferred end <= (a,b: CURRENT): BOOLEAN ensure Result = (a=a*b) end >= (a,b: CURRENT): BOOLEAN ensure Result = (a+b=b) end -- ... "<", ">" defined in the usual manner (not important for the -- following) end feature -- Axioms all(a,b,c:CURRENT) deferred ensure -- strong equality a=b => a~b -- commutative a*b = b*a a+b = b+a -- associative a*b*c = a*(b*c) a+b+c = a+(b+c) -- absorption a = a*(a+b) a = a+(a*b) -- distributive a*(b+c) = a*b+a*c a+(b*c) = (a+b)*(a+c) end end
With this operations and axioms a couple of assertions have already been
proved.
feature -- Proved assertions all(a,b,c:CURRENT) ensure a=a (a=b) = (b=a) a=b => b=c => a=c a*a = a a+a = a a*b <= a a*b <= b c<=a => c<=b => c<=a*b a <= a+b b <= a+b a<=c => b<=c => a+b<=c a<=a a<=b => b<=a => a=b a<=b => b<=c => a<=c a<=b = b>=a end end
For a descendant of “distributive_lattice” it is not important how the proofs
have been done.
The module “boolean_lattice” has the following basic definitions in addition to
the already inherited ones.
deferred class BOOLEAN_LATTICE inherit DISTRIBUTIVE_LATTICE end feature -- Additional operations and axioms 0: CURRENT -- The bottom element. deferred end 1: CURRENT -- The top element. deferred end - (a:CURRENT): CURRENT -- The complement (negation) of `a'. deferred end all(a,b:CURRENT) deferred ensure neutral_0: a+0 = a neutral_1: a*1 = a compl_0: a*(-a) = 0 compl_1: a+(-a) = 1 end end
A compiler analyzing the module “boolean_lattice” discovers that the module
does not have any effective assertion which states strong equality,
commutativity, associativity and distributivity. Therefore it concludes that
these laws are kept as axioms. Because the class BOOLEAN_LATTICE is an
abstract class it is allowed to have axioms
Furthermore the compiler discovers that the absorption laws are stated as
effective assertions within the module (see next chapter). From this fact it
concludes that it has to prove the absorption laws without using any inherited
derived assertions.
In a boolean lattice it is possible to prove the idempotence laws of the join
and meet operation based on the distributivity and the neutrality axioms.
feature -- Idempotence all(a:CURRENT) check c1: a*a + a*(-a) = a*a -- compl_0, neutral_0 c2: (a+a)*(a+(-a)) = a+a -- compl_1, neutral_1 ensure idem_1: a = a*a -- c1, distr, compl_1 idem_2: a = a+a -- c2, distr, compl_0 end end
Having the idempotence laws available it is possible to prove some facts about
“0” and “1”.
feature -- Properties of "0" and "1" all(a:CURRENT) check c1: a*0 = a*(a*(-a)) -- compl_0 c2: a+1 = a+(a+(-a)) -- compl_1 ensure bottom: a*0 = 0 -- c1, assoc, idem, compl_0 top: a+1 = 1 -- c2, assoc, idem, compl_1 max_1: a<=1 -- def "<=", neutral_1 min_0: 0<=a -- def "<=", bottom end end
feature -- Top and bottom are complements all check c1: -0 = (-0) * (0 + (-0)) -- neutral_1, compl_1 c2: -1 = (-1) + 1*(-1) -- neutral_0, compl_0 ensure bot_compl: -0 = 1 -- c1, distr, idem, compl_0, compl_1 top_compl: -1 = 0 -- c2, distr, idem, compl_1, compl_0 end end
With the help of the idempotence laws and the assertions “a*0=0” and “a+1=1”
it is possible to prove the absorption laws.
feature -- Absorption all(a,b:CURRENT) check c1: a*(a+b) = a*1+a*b -- distr, idem, neutral_1 c2: a+a*b =(a+0)*(a+b) -- distr, idem, neutral_0 ensure absorb_1: a*(a+b) = a -- c1, distr, top, neutral_1 absorb_2: a+a*b = a -- c2, distr, bottom, neutral_0 end end
Since the absorptions laws are proved based on the inherited axioms and the
new axioms, all proved assertions inherited from the module
“distributed_lattice” can be used from now on without the danger of creating
circular proofs.
In a boolean algebra the inversion law “-(-a)=a” is valid. Since the inversion
law is an equality, it can be proved by using the law of antisymmetry of the
order relation.
feature -- Involution all(a:CURRENT) check c1: a = a*((-a)+(-(-a))) -- neutral_1, compl_1 c2: a <= -(-a) -- c1, dist, compl_0, neutral_1, def "<=" c3: a = a + (-a)*(-(-a)) -- neutral_0, compl_0 c4: a >= -(-a) -- c3, dist, compl_1, neutral_0, def ">=" ensure -(-a) = a -- antisymm, c2,c4 end end
Negation inverts the order, i.e. “a<=b” is equivalent to “-b <= -a”. The order
reversal law is similar to the contrapositive law of boolean values. Since the
order reversal law is a boolean equivalence, it can be proved by proving the
forward and backward direction.
We use an additional lemma to prove the forward an backward direction. Since
the auxiliary lemma is not interesting to the user we put it into a private
feature block.
feature{NONE} -- Lemma for order reversal reversal_lemma: all(a,b:CURRENT) require a<=b check c1: a = a*b c2: -b = (-b)*(-a) + (-b)*a -- neutral_1, compl_1, dist c3: (-b)*a = (-b)*(a*b) -- c1 c4: (-b)*a = 0 -- c3, com, assoc, compl_0, bottom c5: -b = (-b)*(-a) -- c2, c4, neutral_0 ensure -b <= -a -- c5 end end
With this lemma it is easy to prove the order reversal law.
feature -- Order reversal all(a,b:CURRENT) check c1: (-b <= -a) => (-(-a) <= -(-b)) -- reversal_lemma ensure (a<=b) = (-b <= -a) -- fwd: reversal_lemma, bwd: c1,inversion end end
The laws of de Morgan
-(a*b) = (-a)+(-b) -(a+b) = (-a)*(-b)
are equality laws. They can be proved with the help of the antisymmetry law of
the corresponding order relation. We factor out the needed inequality laws and
put them into a private feature block.
feature{NONE} -- Lemmas for de Morgan lemma_1: all(a,b:CURRENT) check c1: a <= a+b -- inherited c2: b <= a+b -- inherited c3: -(a+b) <= -a -- c1, order reversal c4: -(a+b) <= -b -- c2, order reversal ensure -(a+b) <= (-a)*(-b) -- c3, c4, meet is infimum end lemma_2: all(a,b:CURRENT) check c1: a*b <= a -- inherited c2: a*b <= b -- inherited c3: -a <= -(a*b) -- c1, order reversal c4: -b <= -(a*b) -- c2, order reversal ensure (-a)+(-b) <= -(a*b) -- c3, c4, join is supremum end end
With the help of the lemmas the proof of De Morgan’s laws is straighforward.
feature -- De Morgan all(a,b:CURRENT) check c1: -((-a)+(-b)) <= a*b -- lemma_1, involution c2: -(a*b) <= (-a)+(-b) -- c1, order reversal c3: a+b <= -((-a)*(-b)) -- lemma_2, involution c4: (-a)*(-b) <= -(a+b) -- c3, order reversal ensure -(a*b) = (-a)+(-b) -- c2, lemma_2, antisymm -(a+b) = (-a)*(-b) -- c4, lemma_1, antisymm end end