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