H. Conrad Cunningham
5 September 2017
Acknowledgements: These slides accompany section 3.2, “Evaluation of Functional Programs” from Chapter 3 “Evaluation and Efficiency”" of “Introduction to Functional Programming Using Haskell”.
Advisory: The HTML version of this document may require use of a browser that supports the display of MathML. A good choice as of September 2017 is a recent version of Firefox from Mozilla.
Introduce model for evaluation of functional programs
Characterize time and space complexity of functional programs
Characterize programs that terminate normally with the correct result
Introduce preconditions and postconditions
A symbol (e.g., variable) always represents the same value within some well-defined context (e.g., function body)
Probably most important property of purely functional languages
It enables manipulation of code like mathematics – “Equals can be replaced by equals”
Rewriting (reducing) expression to “simpler” equivalent form
replacing subexpression matching left side of equation by right side with argument substitution
replacing primitive application (e.g., +
or *
) by its value
a subexpression that can be reduced
By position in expression
leftmost redex first – leftmost reducible subexpression before any other
rightmost redex first – rightmost reducible subexpression before any other
By whether contained within another redex
outermost redex first – reducible subexpression not contained within another redex before others
innermost redex first – reducible subexpression not containing another redex before others
Haskell uses leftmost outermost redex first
fact1 :: Int -> Int
fact1 n = if n == 0 then
1
else
n * fact1 (n-1)
Consider else
clause with n
with value 2
2 * fact1 (2-1)
Multiplication not reducible because need both arguments
Redex 2-1
is innermost
Redex fact1 (2-1)
is outermost
fact1 2 |
|
{ replace fact1 2 using definition } |
|
if 2 == 0 then 1 else 2 * fact1 (2-1) |
|
{ evaluate 2 == 0 } |
|
if False then 1 else 2 * fact1 (2-1) |
|
{ evaluate if } |
|
2 * fact1 (2-1) |
|
{ replace fact1 (2-1) using definition, add implicit parentheses } |
|
2 * (if (2-1) == 0 then 1 else (2-1) * fact1 ((2-1)-1)) |
|
{ evaluate 2-1 in condition } |
2 * (if 1 == 0 then 1 else (2-1) * fact1 ((2-1)-1)) |
|
{ evaluate 1 == 0 } |
|
2 * (if False then 1 else (2-1) * fact1 ((2-1)-1)) |
|
{ evaluate if } |
|
2 * ((2-1) * fact1 ((2-1)-1)) |
|
{ evaluate leftmost 2-1 } |
|
2 * (1 * fact1 ((2-1)-1)) |
|
{ replace fact1 ((2-1)-1) using definition, add implicit parentheses } |
|
2 * (1 * (if ((2-1)-1) == 0 then 1 |
|
else ((2-1)-1) * fact1 ((2-1)-1)-1)) |
|
{ evaluate 2-1 in condition } |
2 * (1 * (if (1-1) == 0 then 1 |
|
else ((2-1)-1) * fact1 ((2-1)-1)-1)) |
|
{ evaluate 1-1 in condition } |
|
2 * (1 * (if 0 == 0 then 1 |
|
else ((2-1)-1) * fact1 ((2-1)-1)-1)) |
|
{ evaluate 0 == 0 } |
|
2 * (1 * (if True then 1 |
|
else ((2-1)-1) * fact1 ((2-1)-1)-1)) |
|
{ evaluate if } |
|
2 * (1 * 1) |
|
{ evaluate 1 * 1 } |
2 * 1 |
|
{ evaluate 2 * 1 } |
|
2 |
Rewriting model in example uses string reduction
represent expression as string
replace a substring by equivalent string
But sometimes results in repeated reductions of same subexpression
Below subsequently reduces (2-1)
three times
2 * fact1 (2-1) |
|
2 * (if (2-1) == 0 then 1 else (2-1) * fact1 ((2-1)-1)) |
More efficient rewriting model uses graph reduction
use directed acyclic graph to represent expression
make repeated argument use shared subgraph
replace one subgraph by equivalent subgraph
Below reduces shared occurrences of (2-1)
in one step
2 * fact1 (2-1) |
|
2 * (if (2-1) == 0 then 1 else (2-1) * fact1 ((2-1)-1)) |
|
2 * (if 1 == 0 then 1 else 1 * fact1 (1)-1)) |
fact1 2 |
|
{ replace fact1 2 using definition } |
|
if 2 == 0 then 1 else 2 * fact1 (2-1) |
|
{ evaluate 2 == 0 in condition } |
|
if False then 1 else 2 * fact1 (2-1) } |
|
{ evaluate if } |
|
2 * fact1 (2-1) |
|
{ replace fact1 (2-1) using definition, add implicit parentheses } |
|
2 * (if (2-1) == 0 then 1 else (2-1) * fact1 ((2-1)-1)) |
|
{ evaluate 2-1 because of condition (3 occurrences in graph) } |
|
2 * (if 1 == 0 then 1 else 1 * fact1 (1-1)) |
|
{ evaluate 1 == 0 } |
2 * (if False then 1 else 1 * fact1 (1-1)) |
|
{ evaluate if } |
|
2 * (1 * fact1 (1-1)) |
|
{ replace fact1 ((1-1) using definition, add implicit parentheses } |
|
2 * (1 * (if (1-1) == 0 then 1 else (1-1) * fact1 ((1-1)-1)) |
|
{ evaluate 1-1 because of condition (3 occurrences in graph) } |
|
2 * (1 * (if 0 == 0 then 1 else 0 * fact1 (0-1)) |
|
{ evaluate 0 == 0 } |
|
2 * (1 * (if True then 1 else 0 * fact1 (0-1)) |
|
{ evaluate if } |
2 * (1 * 1) |
|
{ evaluate 1 * 1 } |
|
2 * 1 |
|
{ evaluate 2 * 1 } |
|
2 |
number of steps in leftmost outermost graph reduction
fact1 n
requires 5n + 3
reductions
Alternatively count dominant operations – n
multiplications, n
recursive calls
Time complexity: O(n
)
maximum graph size needed for leftmost outmost graph reduction
Size of expression graph is total number of operands
Largest expression for fact1 2
has 20 operands
2 * (1 * (if (1-1) == 0 then 1 else (1-1) * fact1 ((1-1)-1))
Largest expression for fact1 n
has size 2n + 16
n
] plus 16 for full if
expressionSpace complexity: O(n
).
Each recursive call gets closer to base case
For n > 0
, fact1 n
calls fact1 (n-1)
For n == 0
, fact1
terminates normally
For n < 0
, fact1
does not terminate normally
Logical assertion that caller (client) must ensure holds before function call
Specifies valid combinations of values of arguments and global structures accessed or modified
Means called function expected to terminate with correct result
Function fact1 n
has precondition n >= 0
to prevent infinite recursion
If precondition holds, then function must terminate with this logical assertion satisfied
Function fact1
has postcondition fact1 n =
fact’(n)
(defined in Chapter 2)
fact5
fact5 :: Int -> Int
fact5 n = product [1..n]
fact1
True
fact1
fact5 n = if n >= 0 then
fact’(n) else 1
Logical assertion that always holds for every “object” created by public constructors and manipulated only by public operations
Postcondition of constructor functions
Precondition and postcondition of all functions that access or update “object”
Precondition of destructor functions that explicitly release resources of “object”
Referential transparency
Reduction strategies (leftmost vs. rightmost, innermost vs. outermost)
String and graph reduction models
Time and space complexity
Termination
Preconditions, postconditions, and invariants
The Haskell code for this chapter are in file EvalEff.hs
.