Three Inter-Related Ideas:
Low-Level Functional Programming
Inductive Programming
Recursive Programming
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/Nimo.html)
The
Secret of Nim
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