Three Inter-Related Ideas:
Low-Level Functional Programming
Inductive Programming
Recursive Programming
Robert M. Keller
Copyright 1997 by Robert M. Keller, all
rights reserved.
Low-Level Functional Programming:
Construct basic functions used for
high-level functional programming
Provides "glue" for certain functions
Inductive Programming:
Define functions
starting with minimal information structures
then moving to more general information structures
Recursive Programming:
Define a function on a complex information structure
by
appealing to the definition of the function on simpler
information structures
Recursion Manifesto
Let recursion do the work for you.
A Single Example of All of the Above:
A function which computes the length of a list
Use the fundamental list dichotomy:
Define the function on the empty list:
length( [ ] ) => 0;
This step is called the
basis.
Define the function on a generic non-empty list:
length( [ A | L ] ) => length(L) +
1;
This step is called the
induction
rule.
This two-step paradigm is used repeatedly.
The above definition is simultaneously:
- low-level:
- We prefer to use
length without
regard to how its defined;
this gives us details.
- inductive:
- We start with the simplest list [ ], then
progress to a general list.
- recursive:
- In computing length( [ A | L ] ) we use the fact
that length(L) already makes sense.
- elegant:
- It is difficult to conceive of a simpler, more
obviously-correct, definition.
Rule evaluation may be understood by
rewriting when necessary:
length([2, 3, 5, 7])
=> (length([3, 5, 7]) + 1)
=> ((length([5, 7]) + 1) + 1)
=> (((length([7]) + 1) + 1) + 1)
=> ((((length([ ]) + 1) + 1) + 1) + 1)
=> (((( 0 + 1) + 1) + 1) + 1)
=> ((( 1 + 1) + 1) + 1)
=> (( 2 + 1) + 1)
=> ( 3 + 1)
=> 4
Another example:
append(L, M)
creates a list with the elements of M followed by those of M
append([1, 2], [3, 4, 5])
==> [1, 2, 3, 4, 5]
==> designates the
result of a series of
rewrites, rather than a single rule
Here there is a question about which list should be the one on
which induction is done; it is not advisable to use both, as this
complicates the definition.
Coding append:
It turns out that the first argument is the proper one for
induction in this case:
Basis:
append( [ ], M )
=> M;
Induction rule:
append( [A | L], M )
=> [A | append(L, M)];
This definition is comparably elegant to length.
Note that the append rule focuses on the current
situation, rather than the situation at the top-level call
to append.
Rewriting with Append:
Append can also be understood by
rewriting, if necessary:
append([1, 2, 3], [4, 5])
=> [1 | append([2, 3], [4, 5]) ]
=> [1, 2 | append([3], [4, 5]) ]
=> [1, 2, 3 | append([ ], [4, 5]) ]
=> [1, 2, 3 | [4, 5] ]
=> [1, 2, 3, 4, 5]
It is usually preferred to not have to go through the rewriting
steps in order to understand the defintion.
What you always wanted to know about rex:
The name rex stands for
rewriting
expressions, and comes from the fact
that functions defined in it can be understood by rewriting.
The rule-ordering convention used in rex is:
Try rules in succession, starting at the first, until
one matches the arguments.
For each function call, start at the top, from the first rule.
Rules with Function Arguments:
These are treated the same as any other rule. The only difference
is that functions may be applied:
map(F, [ ] ) => [ ];
map(F, [A | L]) => [F(A) | map(F, L)];
reduce( _, Unit, [ ] ) => Unit;
reduce( H, Unit, [A | X] ) => H(A, reduce(H, Unit, X));
Guarded Rules:
In addition to the discrimination provided by argument matching,
discrimination can also be provided by a guard. The rule is
applicable only in the case the guard evaluates to true.
Trumped up example:
foo( [ ] ) => [1];
foo( [ A | L] ) => length(L) > 5 ? [ 1 | L ];
----------------
foo(L) => L;
The guard is shown by the underline in the second rule.
Sample evaluations:
foo( [ ] ) ==> [1]
foo( [ 2, 3, 4, 5, 6, 7] ) ==> [1, 2, 3, 4, 5, 6, 7]
foo( [2, 3, 4] ) ==> [2, 3, 4]
change_first example from notes, chapter 3:
Used in devising a function for playing the game of nim:
Captain
Nimo applet (http://www.cs.hmc.edu/~keller/javaExamples/Nimo/Nimo.html)
change_first(P, F, L)
creates a new list from L by finding the first element satisfying
predicate P and applying the function F to it, leaving other elements
unchanged.
Low-level coding of change_first:
change_first(P, F, []) => [];
change_first(P, F, [E | L]) => P(E) ? [F(E) | L];
change_first(P, F, [E | L]) => [E | change_first(P, F, L)];
Conditional Expressions:
Similar to guarded rules, but provide an alternative (following
:) for the case when the guard is not satisfied:
bar( L ) => length(L) > 5 ? [ 1 | L ] : L;
^
note
----------------------------
conditional expression
The syntax for conditional expressions is the same as in
C/C++/Java.
Equational Guards:
Define variables for local use inside of a rule
f(X) => Y = X*X, g(Y);
--------
equational guard
Equational guards can be used for pattern-matching in lists:
f(X) => [F | R] = X, g(F*F, R);
-----------
equational guard defining F and R from X
Examples and Problems
Remember to let recursion do the work for
you.
Problem:
Give rules for converting a list of 0's and 1's representing a
binary numeral, least-significant digit first, to a number.
fromBinary( [0, 1, 0, 1, 0, 1] ) ==> 42
Basis:
fromBinary([ ]) => 0;
Induction rule:
fromBinary( [ LeastSignificantDigit | Digits ] ) =>
Problem:
Give rules for converting a number to a list of 0's and 1's
representing a binary numeral, least-significant digit first.
toBinary(42) ==> [0, 1, 0, 1, 0, 1]
Here the argument is not a list, so the basis won't be [ ].
Instead it will be the number 0.
First approximation:
Basis:
toBinary(0) => [0];
Induction rule:
toBinary(N) => [ | ];
In the second rule, N can be assumed to be other than 0. Zero
would be captured by the first rule.
The rules above don't quite work; they always puts a high-order 0
at the end of the list. This is harmless, but not really desired. To
avoid this, we need to handle the argument 0 separately.
toBinary(0) => [0];
toBinary(N) => toBinary1(N);
toBinary1(0) => [];
toBinary1(N) => [ N % 2| toBinary1(N / 2) ];
Here
toBinary is the
interface function and
toBinary1 is the
helper or
auxiliary
function.
Problem:
Give rules for converting a list of 0's and 1's representing a
binary numeral, most-significant digit first, to a number.
The pedestrian solution is just to use the previous function
fromBinary but call it on a reversed argument:
fromBinaryMSD(List) = fromBinary(reverse(List));
How can we do it without reversing the list first?
Conversion from Binary, MSD first:
A solution from scratch is tricky. We want to avoid computing
powers of 2 explicitly. We use an auxiliary which returns a list of
two arguments, a conversion in progress and a power of 2.
This is a good example of using equational guards:
from_binary_msbf(Bits) =
[Number, Power] = from_binary_msbf1(Bits),
Number;
from_binary_msbf1([]) => [0, 1];
from_binary_msbf1([Bit | Bits]) =>
[Number, Power] = from_binary_msbf1(Bits),
[Bit*Power+Number, Power*2];
from_binary_msbf1 returns the number and the next higher power of
2
Problem:
Give rules for converting a number to a list of 0's and 1's
representing a binary numeral, most-significant digit first.
As before, the pedestrian solution is just to use the previous
function toBinary but reverse the result:
toBinaryMSD(Number) = reverse(toBinary(Number));
How can we do it without reversing the list after?
We use the idea of an accumulator, an argument which accumulates
a step at a time the value to be ultimately returned.
The interface function handles the case of argument 0:
to_binary_msbf(0) => [0];
to_binary_msbf(N) => to_binary_msbf(N, []);
to_binary_msbf(0, Acc) => Acc;
to_binary_msbf(N, Acc) => to_binary_msbf(N/2, [N%2 | Acc]);
The accumulator in the second function builds up the ultimate
result inside-out, starting with the least-significant digit:
Problem:
Construct function pick in the programming language of
your choice.
pick(Goal, Items)
where Goal is a non-negative integer and Items
is a sequence of positive integers, yields a pair:
The first element of the result pair is the
sum of the elements in the second element, as defined next.
The second element of the pair is a set of integers in
Items which has a sum (there many be more than one) that is
closest to Goal without exceeding it, where each item can be
used at most once.
For example:
pick(4, [1, 8, 3, 5]) ==> [4, [1, 3]]
pick(10, [1, 3, 3, 5]) ==> [9, [1, 3, 5]]
pick(0, [1, 4, 3, 5]) ==> [0, []]
In the first example, 1+3 == 4 exactly. In the second, 1+8 == 9,
the closest we can come to 10 with the numbers given. In the third
case, choosing the empty list sums to 0 exactly.
What solution paradigm should be used?
pick(Goal, []) => [0, []]; // empty list can only meet goal 0
pick(Goal, [Item | Items]) =>
Item > Goal ? pick(Goal, Items) // Item too large to use
: [Sum1, Items1] = pick(Goal-Item, Items), // Try with Item
[Sum2, Items2] = pick(Goal, Items), // Try without Item
Sum1 + Item > Sum2 ? // Which is closer?
[Sum1+Item, [Item | Items1]] // Using item
: [Sum2, Items2]; // Not using item