A function is a named body of commands that do significant work. When we define a function, we should summarize its knowledge-production capabilities. This is critical when we assemble a program from a collection of functions, because we connect functions together based on what they are capable of doing.
A function is a ``mini-program'': it has inputs, namely the arguments assigned to its parameters, and it has outputs, namely the information returned as the function's answer. It acts like a logic gate or chip, with ``input wires'' and ``output wires.''
Here is an example that shows the format we use for writing a function:
===================================================
def recip(n) : # HEADER LINE : LISTS NAME AND PARAMETERS
"""recip computes the reciprocal for n"""
"""{ pre n != 0 # PRECONDITION : REQUIRED PROPERTY OF ARGUMENTS
post ans == 1.0 / n # POSTCONDITION : THE GENERATED KNOWLEDGE
return ans # VARIABLE USED TO RETURN THE RESULT
}"""
ans = 1.0 / n # BODY (no assignment to parameter n allowed)
return ans
===================================================
The precondition states the situation under
which the function operates correctly, and the postcondition
states what the function has accomplished when it terminates.
For simplicity, we require that no assignment is made to a parameter inside the body of the function. In a call-by-value language like Python and Java, this is never a handicap.
The functions's specification consists of its
header line and its pre-, post-condition, and return lines;
it tells us how
to use the function correctly: (The comment with the
informal description is also nice to have.)
Here is the specification for the above function:
def recip(n) :
"""recip computes the reciprocal for n"""
"""{ pre n != 0
post ans == 1.0 / n
return ans
}"""
The specification tells us what we need to know to
use the function correctly. The person/program who calls
the function is not supposed to read the function's code to know
how to use the function. This is especially crucial when
you use a function from a library that was written by someone
else --- you should not read the library's code to learn
how to call the library's functions!
Back to the example: Since we do not allow assignments to parameter n within the body of recip, we know that the value of n in the postcondition is the same as the value of n in the precondition --- this makes the postcondition meaningful.
To call the function, we supply an argument that binds to
(is assigned to) its parameter,
and we supply a variable that is the target for the returned
answer. Once again, for this specification:
def recip(n) :
"""recip computes the reciprocal for n"""
"""{ pre n != 0
post ans == 1.0 / n
return ans
}"""
we correctly call the function like this:
===================================================
a = readInt()
if a > 0 :
"""{ 1. a > 0 premise
2. a != 0 algebra 1 # this proves recip's precondition
}"""
b = recip(a)
"""{ 1. b == 1.0 / a premise }""" # in return, we presume its postcondition
print "the reciprocal of", a, "is", b
else :
. . .
===================================================
To call the function, we must prove the function's
precondition at the point of call for its argument.
As our reward for establishing the precondition, we receive in
return the postcondition, stated in terms of a and b:
b == 1.0 / a.
For simplicity, we require that the target variable that receives the function's answer does not appear in the function's arguments, e.g., the call, b = recip(b+2), is not allowed, because it confuses the b in the argument to recip with the b in the answer.
In contrast, we cannot presume recip's
postcondition in this situation:
===================================================
x = readInt()
# We do not know if x is nonzero....
y = recip(x)
"""{ ??? }"""
===================================================
Since
we did not prove x != 0 as a precondition for applying
recip,
we cannot assert the postcondition after the call. Literally,
we have no knowledge of what the function produces in this situation ---
perhaps the call will ``crash'' (generate an exception)!
(We rely on the function's specification to tell us how
to use the function, and we are not supposed to dig into the function's
code to guess what might happen with an ``illegal'' call.)
Here is a summary of what we have developed so far.
Given a function, f, with a pre-post specification,
we must show that the presumption of the precondition at the beginning
of the function's body lets us prove the postcondition at the end of the body:
===================================================
def f(x):
"""{ pre Q (where assertion Q can mention x) Call this, f_pre.
post R (where assertion R can mention ans and x) Call this, f_post.
return ans (the name of the answer variable)
}"""
"""{ 1. Q premise }"""
BODY # does not assign to x
"""{ ... prove R here, that is, ans and x have property R }"""
===================================================
Outside of the function, f, we write
f_pre for Q and
f_post for R.
Here is the schematic for a correct call of the function:
===================================================
"""{ [e/x]f_pre }""" # prove the precondition, where e binds to parameter x
y = f(e) # y does not appear in e
"""{ 1. [y/ans][e/x]f_post premise # presume the postcondition where
# y receives the ans value
2. [e/x]f_pre premise # knowledge before the call is updated
. . .
}"""
===================================================
Recall that
[e/x]f_pre defines a substitution of e for all occurrences of x
within formula f_pre.
The other two substitions explain how f_post is stated in terms of
the receiving variable, y, and how the entry premise is propagated
after the call.
Here is another example,
an absolute-value function:
===================================================
def absValue(x) :
"""{ pre x != 0
post ans > 0
return ans
}"""
if x < 0 :
ans = 0 - x
else :
ans = x
return ans
===================================================
The precondition is crucial to the success
of the postcondition.
We must construct the proof that
the function's body converts the knowledge
in the precondition into the knowledge stated in the postcondition:
===================================================
def absValue(x) :
"""{ pre x != 0
post ans > 0
return ans
}"""
if x < 0 :
"""{ 1. x != 0 premise
2. x < 0 premise
}"""
ans = 0 - x
"""{ 1. x < 0 premise
2. ans == 0 - x premise
3. ans > 0 algebra 1 2
}"""
else :
"""{ 1. x != 0 premise
2. not(x < 0) premise
3. x >= 0 algebra 2
4. x > 0 algebra 1 3
}"""
ans = x
"""{ 1. ans == x premise
2. x > 0 premise
3. ans > 0 subst 1 2
}"""
"""{ 1. ans > 0 premise }"""
# that is, we proved (ans > 0 or ans > 0), which is ans > 0
return ans # we proved the postcondition
===================================================
Now that the function is certified to satisfy its specification,
we can call it:
===================================================
n = readInt()
if n != 0 :
"""{ 1. n != 0 premise }"""
m = absValue(n)
"""{ 1. m > 0 premise }""" # the function's postcondition
. . .
===================================================
Function specifications are critical to software libraries: Every library component simply must has a specification that describes how to connect the component into an application. Often the specifications are written in English, but underlying the English descriptions are the pre-postconditions illustrated in this section.
If you the person writing the function for others to use, you supply the function's specification. You can calculate the specification using the laws we studied in the previous chapter. But better still, you should start with the specification and write the function so that it matches the specification.
You start by stating, in English or otherwise, the function's goal. You study the goal and consider how to meet it; you write the code. You take note of the requirements your function needs on its entry parameters and global variables to reach the goal. Finally, you apply the programming-logic laws to show that the coding matches the specification.
Here is a simplistic example. Say you must write a function that
receives two numbers as inputs and selects the maximum, or larger,
of the two as the function's output. The specification of the function,
max, might look like this:
===================================================
def max(x, y)
"""max selects the larger of x and y and returns it as its answer"""
"""{ pre ???
post (ans == x v ans == y) ^ (ans >= x ^ ans >= y)
return ...
}"""
===================================================
You must write the code for function max so that it meets the
postcondition. Along the way, you might require some restrictions on
x and y for your solution to work. These would be
listed in the precondition.
The logical operators in the postcondition sometimes give us hints
how to code the function. Here, we see that the function's answer
will be either x or y, suggesting that the function
will use assignment commands of the form,
ans = x and ans = y.
We also see that there is an ``or'' operator in the specification.
This suggests that we will require an if-command to choose which
of the two assignments to use.
Although it is not an exact science, we can move from the postcondition
to this possible coding:
===================================================
def max(x, y)
"""{ pre ???
post (ans == x v ans == y) ^ (ans >= x ^ ans >= y)
return ans }"""
if x > y :
ans = x
else :
ans = y
return ans
===================================================
Now, we use the laws for assignments and conditionals to compute whether
a precondition is needed that places restrictions on x and
y so that the function behaves properly.
What we see here is that our coding works correctly with numbers
(and with strings, too!). But it might not work correctly, say,
if x or y are arrays or pointers.
For this reason, it seems safe to use this precondition:
===================================================
def max(x, y)
"""max selects the larger of x and y and returns it as its answer"""
"""{ pre x and y are both numbers or both strings
post (ans == x v ans == y) ^ (ans >= x ^ ans >= y)
return ans }"""
if x > y :
ans = x
else :
ans = y
return ans
===================================================
Now, we have a function --- a component --- that others can
insert into their programs.
def f(x): """{ pre Q post R return ans }"""If a call to f is meant to achieve a goal, G, we reason backwards from G to its subgoal like this:
=================================================== """{ subgoal: [e/x]f_pre ^ ([e/x][y/ans]f_post --> G) }""" y = f(e) # y does not appear in argument e """{ goal: G }""" ===================================================That is, for the call, f(e), to accomplish G, it must be the case that f's postcondition implies G and that we can prove f's precondition holds true (so that we can call f).
An example: for
===================================================
def recip(n) :
"""{ pre n != 0
post ans == 1.0 / n
return ans
}"""
===================================================
and the program
===================================================
a = readInt()
"""{ subgoal: ??? }"""
b = recip(a)
"""{ goal: b < 1.0/3 }"""
===================================================
we compute that the subgoal is
a != 0 ^ (b = 1.0/a --> b < 1.0/3).
This simplifies to
a != 0 ^ (1.0/a < 1.0/3)), which simplifies to
a != 0 ^ a > 3, which simplifies to a > 3.
This tells us what assert or if command to add to our program:
===================================================
a = readInt()
assert a > 3
"""{ subgoal: a > 3 }"""
b = recip(a)
"""{ goal: b < 1.0/3 }"""
===================================================
That is, if we expect the computed reciprocal to be less than
one-third, then we must supply an input int that is 4 or more.
Say that a variable, v, is ``global'' to a function f
if v exists prior to a call to f. (More precisely stated,
v's cell does not live in f's namespace/activation-record.)
For example, pi is global to circumference here:
===================================================
pi = 3.14159
def circumference(diameter) :
answer = pi * diameter
return answer
. . .
r = readFloat("Type radius of a circle: ")
c = circumference(2 * r) # compute circumference of circle
area = r * r * pi # compute circle's area
===================================================
A global variable ``lives'' before a function is called and lives
after the function finishes.
In the above example, pi is defined before the function is called, and
it exists after the function finishes. In contrast,
pi is local here:
===================================================
def circ(d) :
pi = 3.14159 # pi is created new each time circ is called
answer = pi * diameter
return answer
. . .
r = readFloat("Type radius of a circle: ")
c = circumference(2 * r)
# pi does not exist here; it cannot be referenced:
area = r * r * pi # ???!
===================================================
A global variable that is read (but not updated) by a function body can
be safely used in the function's pre- and postconditions; it acts just like
an extra parameter to the function:
===================================================
pi = 3.14159
def circumference(diameter) :
"""{ pre diameter >= 0 ^ pi > 3
post answer = pi * diameter
return answer }"""
answer = pi * diameter
return answer
===================================================
We must restrict all calls to the function so that the call
does not use the global variable as the target of
the function's answer. That is,
c = circumference(5) is ok, and so is
c = circumference(pi), but
pi = circumference(5) is not.
In the Python language, every global variable that is updated by a function must be listed in the global line that immediately follows the function's header line. This is a good practice that we will follow.
Here is a first, simple example:
===================================================
pi = 3.14159
c = 0
def circumference(diameter) :
"""{ pre diameter >= 0 ^ pi > 3
post c = pi * diameter
}"""
global c # will be updated by this procedure
c = pi * diameter
. . .
d = readFloat("Type diameter of a circle: ")
assert d >= 0
"""{ 1. d >= 0 premise
2. pi == 3.14159 premise
3. pi > 3 algebra 2
4. d >= 0 ^ pi > 3 ^i 1 3
# we proved the precondition for circumference
}"""
circumference(d)
"""{ 1. c == pi * d premise }""" # we get its postcondition
print c
===================================================
The answer computed by procedure circumference is deposited
in global variable, c. For this reason, the procedure's
postcondition is stated in terms of diameter and
and c, and the postcondition applies after the procedure is
called.
Here is a precise statement of the law we use for procedures:
===================================================
g = ... # the global variable
def f(x):
"""{ pre Q (where assertion Q mentions x and g) This is f_pre.
post R (where R mentions ans, x, and g) This is f_post.
return ans
}"""
global g # notes that g can be updated by f
BODY # does not assign to x but may assign to g
return ans # this line is optional
===================================================
To invoke the function,
we prove the precondition:
===================================================
"""{ [e/x]f_pre }"""
y = f(e) # y and g do not appear in e, and y and g are distinct
"""{ 1. [y/ans][e/x]f_post premise
2. [y_old/y][g_old/g][e/x]f_pre premise
...
}"""
===================================================
Since global variable g acts as a second answer variable, g cannot appear
in argument e nor can it be the same as y.
Whenever a module or class holds a shared data structure, there should be a global invariant that states the critical properties of the structure that must be maintained. The procedures that update the structure pledge to preserve the global invariant.
This is the technique used in modular and object-oriented programming, where a data structure, its global invariant, and the data structure's update functions are placed together. Since only the maintenance functions update the global variable and preserve the invariant, the rest of the program can always rely on the global invariant to hold true. Without this convention, it is impossible to conduct proper component-based programming.
Here is a tiny example that makes this point:
It is a module/class that models a bank account with a global
variable and two maintenance functions:
The variable's value should always be kept nonnegative:
===================================================
# A bank-account "class" and its maintenance functions.
# The balance balance must be kept nonnegative.
# The global variable, the money in a bank balance:
balance = 0
"""{ 1. balance == 0 premise
2. balance >= 0 algebra 1
}"""
# the global invariant:
"""{ globalinv balance >= 0 }""" # this property is currently true, and
# we want the functions to preserve it
def deposit(howmuch) :
"""deposit adds howmuch to balance"""
"""{ pre howmuch >= 0
post balance == balance_in + howmuch }"""
# balance_in is the value of balance when the method was entered
global balance
"""{ 1. balance >= 0 premise # the globalinv holds on entry
2. howmuch >= 0 premise # the function's precondition
3. balance == balance_in premise # the value of balance on entry
4. return 1 2 3 # remember all three facts for later
}"""
balance = balance + howmuch
"""{ 1. balance == balance_old + howmuch premise
2. balance_old >= 0 premise
3. howmuch >= 0 premise
4. balance_old == balance_in premise
5. balance == balance_in + howmuch subst 4 1
6. balance >= 0 algebra 1 2 3
7. return 5 6
}"""
# The global invariant is preserved at the procedure's exit,
# and the postcondition is proved, too.
def withdraw(howmuch) :
"""withdraw removes howmuch from balance"""
"""{ pre howmuch >= 0
post balance == balance_in + cash
return cash }"""
global balance
if howmuch <= balance :
balance = balance - howmuch
cash = howmuch
else :
cash = 0
# An exercise: prove here balance == balance_in + cash and balance >= 0
return cash
def getBalance() :
"""getBalance returns the current balance"""
"""{ pre True
post ans == balance
return ans
}"""
ans = balance
return ans
# Later in the program, wherever it's needed, we can assert:
"""{ ...
k. balance >= 0 globalinv
... }"""
# but we CANNOT do any assignments to balance within the program;
# we call the functions, deposit and withdraw, to do the assignments.
===================================================
The two functions, deposit and withdraw, pledge to always keep
balance's value nonnegative. Assuming that no other program
commands update the balance, the proofs of the two functions
suffice to ensure that balance's global invariant holds
always.
Here is the law used in the above example:
===================================================
g = ... # the global variable
"""{ globalinv I_g }""" # must be proved true here
def f(x):
"""{ pre Q (where assertion Q mentions x and g) This is f_pre.
post R (where R mentions ans, x, and g) This is f_post.
return ans
}"""
global g # g can be updated by f
"""{ 1. Q premise
2. I_g premise
...
}"""
BODY # does not assign to x but may assign to g
"""{ R ^ I_g }""" # we must prove both R and I_g on exit
return ans
===================================================
For the function's invocation,
we deduce [e/x]pre_f to get the result.
Since global variable g acts as a second answer variable, g cannot appear in argument e nor can it be the same as y.
===================================================
"""{ [e/x]pre_f, that is, Q_e,g }"""
y = f(e) # y and g do not appear in e, and y and g are distinct names
"""{ 1. [y/ans][e/x]post_f premise
2. [y_old/y][g_old/g][e/x]pre_f premise
3. I_g premise
...
}"""
===================================================
Further, provided that all assignments to global variable g occur
only within functions that preserve its I_g, we can always assert
I_g as needed in the main program.
In the previous example, there is another critical invariant: the account balance correctly totals all the transactions made to the bank account, that is, the balance is always correct. If we add a log to the component, we can state this property as an invariant, too.
We add an array (list), named log, and append to it all
the amounts deposited and withdrawn.
For example, if an account starts with a deposit of 100
followed by a withdrawal of 50 and a deposit of 10, we would
have
log = [ +100, -50, +10 ]
and it must be that variable account == 60.
The relationship is stated as this invariant:
account == summation i: 0..len(log)-1, log[i]
that is,
account == log[0] + log[1] + ... + log[len(log)-1],
where len(log) is the length of array/list, log.
Here is the revised coding of the component:
===================================================
### A MODULE/CLASS THAT MODELS A BANK ACCOUNT ###############
# The global variable, the money in the account:
account = 0
"""{ globalinv account >= 0 }"""
# A real account maintains a log of all transactions:
log = [0] # log is an array (list) that grows.
"""{ globalinv account == summation i:0..log(len-1), log[i] }"""
# We can use def clauses, explained in later in this Chapter, and arrays,
# explained in Chapter 6, to state and prove this property.
def deposit(howmuch) :
"""deposit adds howmuch to account"""
"""{ pre howmuch >= 0
post ...see earlier example... }"""
global account, log
account = account + howmuch
log.append(+howmuch) # record the transaction in the log
# We must prove the global invariants are preserved at the exit.
def withdraw(howmuch) :
"""withdraw removes howmuch from account"""
"""{ pre howmuch >= 0
post ...see earlier example...
return cash }"""
global account, log
if howmuch <= account :
log.append(-howmuch) # record the transaction in the log
balance = balance - howmuch
cash = howmuch
else :
cash = 0
# We reprove the global invariants here.
===================================================
There are many other examples of modules/classes that use global invariants:
=================================================== def recip(n) : """{ pre n != 0 post ans == 1.0 / n return ans }""" ans = 1.0 / n return ans def main() """{ pre True post (x !=0) --> (y == 1.0 / x) }""" x = readFloat("Type a number: ") if x != 0 : y = recip(x) # call recip here, since its precondition holds true print y else : print "can't compute reciprocal" ===================================================The call merely appears inside the body of a procedure. In this way, procedures can build on each others' postconditions.
For integer n > 0, the factorial of n, written n! is defined as 1*2*3*...up to...*n. It is the tradition to define 0! = 1, but factorials for negative integers do not exist.
Factorial lists number of permutations,
e.g., fact(3) = 6 notes there six permutations (combinations)
for arranging three items, a, b, and c:
abc
bac
bca
acb
cab
cba
There is a clever recursive definition of factorial, which tersely
states the pattern of multiplications from 1 up to m:
0! == 1
n! == (n-1)! * n, for n > 0
For example, we calculate 4! like this:
===================================================
4! == 3! * 4
where 3! == 2! * 3
where 2! == 1! * 2
where 1! == 0! * 1
where 0! == 1
So...
0! == 1
1! == 0! * 1 = 1
2! == 1! * 2 = 2
3! == 2! * 3 = 6
and finally,
4! == 3! * 4 = 24
===================================================
The process of counting downwards from 4! to 3! to 2! to 1! to 0!
and assembling the answer as 1 * 1 * 2 * 3 * 4 can be programmed
as a function that repeatedly calls itself for the answers to
3!, 2!, 1!, etc.:
===================================================
def fact(n) : # returns n!
"""{ pre n >= 0
post (to come)
return ans
}"""
if n == 0 :
ans = 1 # this computes 0! = 1
else :
a = fact(n-1) # this computes (n-1)!
ans = a * n # this computes n! = (n-1)! * n
return ans
===================================================
The easiest way to understand the computation of, say, fact(3),
is to draw out the function calls, making a new copy of the
called function each time the function is restarted, like this:
===================================================
fact(3) => n = 3
if n == 0 :
ans = 1
else :
a = fact(n-1)
ans = a * n
return ans
===================================================
Notice how the binding of argument 3 to parameter n is
enacted with the assignment, n = 3. (This is how it is implemented
within a computer, too.)
The code for fact(3) itself
calls (activates a fresh copy of) fact with argument 2:
===================================================
fact(3) => n = 3
a = fact(2) => n = 2
if n == 0 :
ans = a * n ans = 1
return ans else :
a = fact(n-1)
ans = a * n
return ans
===================================================
This generates another call (fresh copy of) fact:
===================================================
fact(3) => n = 3
a = fact(2) => n = 2
ans = a * n a = fact(1) => n = 1
if n == 0 :
return ans ans = a * 2 ans = 1
return ans else :
a = fact(n-1)
ans = a * n
return ans
===================================================
This goes to
===================================================
fact(3) => n = 3
a = fact(2) => n = 2
ans = a * n a = fact(1) => n = 1
return ans ans = a * n a = fact(0) => n = 0
if n == 0 :
return ans ans = a * n ans = 1
return ans else :
. . .
return ans
===================================================
We see a sequence, or ``stack,'' of activations of fact, one per
call. Within a computer, an activation-record stack is
remembers the sequence of activations.
The call to fact(0) returns an answer --- 1:
===================================================
fact(3) => n = 3
a = fact(2) => n = 2
ans = a * n a = fact(1) => n = 1
return ans ans = a * n a = fact(0) =>
return ans ans = a * n return 1
return ans
===================================================
This makes the most recent activation (copy) of fact disappear, and
the returned answer is assigned to ans in the previous call,
===================================================
fact(3) => n = 3
a = fact(2) => n = 2
ans = a * 3 a = fact(1) => n = 1
return ans ans = a * 2 a = 1
return ans ans = a * n
return ans
===================================================
allowing the previous call to return its answer to its
caller:
===================================================
fact(3) => n = 3
a = fact(2) => n = 2
ans = a * 3 a = fact(1) =>
return ans ans = a * 2 return 1
return ans
===================================================
You see the pattern --- the calls from fact(3) to fact(2) to
fact(1) to fact(0) are finishing in reverse order and are
returning the partial answers,
fact(0) = 1, fact(1) = 1, fact(2) = 2, and so on:
===================================================
fact(3) => n = 3
a = fact(2) => n = 2
ans = a * n a = 1
return ans ans = a * n
return ans
===================================================
and then
===================================================
fact(3) => n = 3
a = fact(2) =>
ans = a * n return 2
return ans
===================================================
and
===================================================
fact(3) => n = 3
a = 2
ans = a * n
return ans
===================================================
and finally
===================================================
fact(3) => return 6
===================================================
Within the computer, the code for fact is not copied at each call --- instead,
a new namespace (activation record) is generated for
each call to fact. When a call finishes, its answer is returned and its
namespace is erased (popped from the activation stack).
Every function has its own pre- and post- conditions. So, if a function calls itself, it can use its own pre- and postconditions to deduce the properties of the answer computed by the self-call. This is remarkable and exactly correct.
Here is the desired specification of fact:
def fact(n)
"""{ pre n >= 0
post ans == n!
return ans
}"""
When fact calls itself, we use the above pre- and
postconditions to deduce what happens. In the process, we
deduce that the completed coding of fact possesses exactly
these same pre- and postconditions!
Recall again the ``official definition'' of n!:
0! == 1
n! == (n-1)! * n, for n > 0
Here is the deduction that fac meets its
stated pre- and postconditions:
===================================================
def fact(n) :
"""{ pre n >= 0
post ans == n!
return ans
}"""
if n == 0 :
ans = 1
"""{ 1. ans == 1 premise
2. n == 0 premise
3. ans == 0! definition of 0! == 1
4. ans == n! subst 2 3
}"""
else :
"""{ 1. ~(n == 0) premise
2. n > 0 premise
3. n - 1 >= 0 algebra 1 2 # this proves pre_fac
}"""
sub = fact(n-1)
"""{ 1. sub == (n-1)! premise }""" # post_fac
ans = sub * n
"""{ 1. ans == sub * n premise
2. sub == (n-1)! premise
3. ans == (n-1)! * n subst 2 1
4. ans == n! definition of n! == (n-1)! * n
}"""
"""{ 1. ans == n! premise }"""
}"""
return ans
===================================================
We did it! By guessing a useful pre- and postcondition, we proved
that the coding of fact that calls itself establishes the
pre- and postconditions.
The proof is not magic or trickery --- notice that the call, fact(n-1), uses an argument value that is different --- one smaller than --- argument n used with fact(n). This style of recursion by counting-downward-by-ones-until-zero will be exposed in the next chapter as an instance of mathematical induction.
Here is a sample call of the end result:
===================================================
"""{ 200 >= 0, that is [200/n]pre_fact }"""
x = fact(200)
"""{ [x/ans][200/n]post_fact, that is, x == 200! }"""
===================================================
In the next chapter, we will see a strong connection between
a function's self-call and a loop --- in both cases, the construct
reuses its very own
``pre-post-condition'' when repeating itself.
=================================================== def f(x): """{ pre Q (where assertion Q can mention x) Call this, f_pre. post R (where assertion R can mention ans and x) Call this, f_post. return ans (the name of the answer variable) }""" """{ 1. Q premise }""" BODY # does not assign to x """{ ... prove R here, that is, ans and x have property R }""" ===================================================Outside of the function, f, we write f_pre for Q and f_post for R. Here is the schematic for a correct call of the function:
=================================================== """{ [e/x]f_pre }""" # prove the precondition, where e binds to parameter x y = f(e) # y does not appear in e """{ 1. [y/ans][e/x]f_post premise # presume the postcondition where # y receives the ans value 2. [y_old/y][e/x]f_pre premise . . . }""" ===================================================Recall that [e/x]f_pre defines a substitution of e for all occurrences of x within formula f_pre. The other two substitions explain how f_post is stated in terms of the receiving variable, y, and how the entry premise is propagated after the call.
=================================================== g = ... # the global variable """{ globalinv I_g }""" # must be proved true here def f(x): """{ pre Q (where assertion Q mentions x and g) This is f_pre. post R (where R mentions ans, x, and g) This is f_post. return ans }""" global g # g can be updated by f """{ 1. Q premise 2. I_g premise ... }""" # pre and global invariant hold true on entry BODY # does not assign to x but may assign to g """{ R ^ I_g }""" # we must prove both post and I_g on exit return ans ===================================================For the function's invocation, we deduce P ^ [e/x]pre_f to get three facts as a result. Since global variable g acts as a second answer variable, g cannot appear in argument e nor can it be the same as y.
=================================================== """{ [e/x]pre_f, that is, Q_e,g }""" y = f(e) # y and g do not appear in e, and y and g are distinct names """{ 1. [y/ans][e/x]post_f premise 2. [y_old/y][g_old/g][e/x]pre_f) premise 3. I_g premise ... }""" ===================================================Further, provided that all assignments to global variable g occur only within functions that preserve its I_g, we can always assert I_g as needed in the main program.
def f(x): """{ pre Q_x post R_ans,x return ans }"""if a call to f is meant to achieve a goal, G, we reason backwards from G to its subgoal like this:
"""{ subgoal: [e/x]pre_f ^ ([e/x][y/ans]post_f --> G) }""" y = f(e) # y does not appear in argument e """{ goal: G }"""It is a good exercise to extend this backwards law to procedures. Try it.