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

To Next Slide To Previous Slide To Contents