Lambda Calculus for EuLisp

This consists of library written in the form of an EuLisp module (lambda-calculus.em) and an application (lc) which uses this library and implements a simple user interface.

1. Data structure used to represent lambda-terms

The data structure which represents the lambda-terms is a class hierarchy.

The base class is <lambda-term>. It is an abstract class with no slots.

Variables are represented by the class <variable>, which derives from <lambda-term>. It has one required slot, name, with reader name and writer alpha-rename.

Applications are stored by the <application> class, also derived from <lambda-term>. It has two read-only required slots, M and N. (Slots are not typed, but it is assumed that these slots will contain <lambda-term> objects.)

Finally the <abstraction> class, also a descendent of <lambda-term>, is used to store abstractions. It has two read-only required slots, parameter (intended for a <variable> object) and body (intended for any <lambda-term> object)

2. Alpha-renaming of lambda-terms

Implementing alpha-renaming of variables is trivial with the structure described above -- the inclusion of the following line in the definition of the <variable> class' name slot declares a function alpha-rename taking as arguments a <variable> and a new name:

      writer: alpha-rename

3. Lambda-term substitution

This is implemented by a set of methods with the following signature:

  (defgeneric substitute ((term <lambda-term>) (search <variable>) (replace <lambda-term>)))

For variables, the implementation of this method returns replace if the variable is search and itself otherwise:

  (defmethod substitute ((term <variable>) (search <variable>) (replace <lambda-term>))
    (if (eq term search) replace term))

For applications and abstractions, the implementations simply recurse into the M and N terms and the body term respectively:

  (defmethod substitute ((term <application>) (search <variable>) (replace <lambda-term>))
    (application (substitute (M term) search replace) (substitute (N term) search replace)))

  (defmethod substitute ((term <abstraction>) (search <variable>) (replace <lambda-term>))
    (abstraction (parameter term) (substitute (body term) search replace)))

Name clashes

The prevention of name clashes is implemented as a separate function, imaginatively called clean-name-clashes. This is called by the interface code after calling substitute.

4. One step beta-reduction of a redex

This is implemented using methods on the <lambda-term> class. Using different methods on each subclass, the behaviour specific to each term type is automatically used. The current implementation hard codes knowledge of abstractions in the application term, but if more types were introduced then a greater level of abstraction could be introduced to remove even that.

See the beta-reduce-step and beta-reduced-p generic functions.

5. General beta-reduction of a lambda-term

This is implemented simply by calling beta-reduce-step until beta-reduced-p returns false.

Heuristic to try to avoid infinite reduction

The interpreter code implements this heuristic by running the beta reduction on a separate thread and waiting for five seconds on the UI thread to see if the reduction terminates. If it does not, then the user is prompted to see if he wants to stop the reduction.

If the reduction is stopped then a condition is signalled on the reduction thread, causing it to abort.

In non-interactive mode (=f file argument) reduction never aborts. Scripts run using lc should therefore avoid trying to reduce expressions that have no normal form. In interactive mode, expressions can be forced to automatically abort after five seconds (without a prompt) by using the =a command line argument.

Examples of Testing

The file test.pl tests the code by running it with some specific expressions, capturing the output, and comparing it to a reference rendering.

Appendix A. lc Manual

To run lc, simply type ./lc in its directory. You must have euscheme installed.

The following commands are supported:

S
returns S.
[R/V]S
returns a new expression consisting of S, where every mention of variable V is replaced by R. The variables are then renamed to remove any name clashes in the new expression. (This may affect variables in other expressions, such as S itself.)
reduce S
returns a new expression that is the beta reduced form of S. Since this may not terminate, you may be prompted to abort the reduction in interactive mode. In batch mode care should be taken to not reduce expressions that do not terminate.
step S
returns a new expression that is S, beta reduced once.
print S
prints and returns S.
copy S
returns a deep copy of S.
variables S
lists the unbound variables in S and returns S.
variable n S
returns the nth unbound variable in S.
rename V a
alpha-renames V to a.

where:

Any command may be prefixed by R= to assign the result of the command to R. Note that the variables used in lambda expressions are in a different namespace to the references. Also bear in mind that two variables with the same name may not actually be the same variable if they were not created in the same expression.

Lines prefixed with a '#' symbol are ignored.

Examples:

  > A=Lx.xy
  Lx.xy
  > B=aa
  aa
  > variables B
  variable 0: a
  aa
  > a=variable 0 B
  a
  > B=[A/a]B
  (Lx.xy)(Lx.xy)
  > reduce B
  yy
  > B
  (Lx.yy)(Lx.yy)
  

To exit the Lambda Calculus interpreter, type: quit

Appendix B. Errors found

During the development of this utility, I found three errors in the EuLisp 0.99 spec:

  1. Section 12.1.8.2, under init-option, contains a typo: to then should be to the.
  2. Section 13.4.3.2, the definition of the and special form, has an inconsistency. The prose says (and) should reduce to () while the rewrite rules say it should reduce to #t.
  3. Section A.1.5.2, the examples for the binary< method, misspells the name of the method four times.

I also found ten bugs in the euscheme interpreter.

  1. <char> should be <character> according to the EuLisp 0.99 spec.
  2. Defining a method apply causes the debugger to fail to respond to its commands (such as :help). This may be because the debugger executes in the scope in which the condition was raised, instead of the scope of the thread.em module, and therefore it is affected by redefinitions of functions it uses.
  3. The (defgeneric) and (generic-lambda) defining forms expect the keyword method: but EuLisp 0.99 spec says that they should expect method (without the colon).
  4. The interpreter doesn't implement EuLisp 0.99's streams.
  5. The order of evaluation of arguments is right-to-left but section 13.2.4.4 of the EuLisp 0.99 specification says it should be left to right.
  6. The debugger's backtraces frequently do not list the actual functions being executed, instead listing the let-binding forms encountered.
  7. Compilation errors do not list line numbers, making debugging long programs a matter of trial and error.
  8. The documentation says that the form of arguments to euscheme is
    euscheme [-opts] [filename] [args...]
    However, it doesn't say that args cannot start with a hyphen (-). This limitation makes it impossible to write shell scripts that accept conventional arguments using euscheme.
  9. <thread> is an abstract class. One has to instantiate a <simple-thread> object when one wishes to make a new thread.
  10. <simple-thread>'s slot is called function: but the EuLisp 0.99 spec says that <thread>'s slot should be called init-function.

Appendix C. Source

The source for the following files is included:

lc
A Lambda Calculus shell script interpreter written in EuLisp that uses euscheme as its own interpreter. This is the core of the program, it just handles the command line arguments and then passes the results of this to the interface module.
lambda-calculus-interface.em
The main read-eval-print loop. Handles reading from standard input and from files. Reads one line at a time and passes each line to the interpreter module for processing
lambda-calculus-interpreter.em
The command parser, this module parses the input from the user and passes it to the core Lambda Calculus code.
lambda-calculus.em
This module implements the majority of the actual coursework.
lambda-calculus-tokeniser.em
In order to fully interpret lambda term expressions, this program uses first a tokeniser, this module, followed by a parser, the next module. These work together to handle nested expressions, implied brackets, and the like.
lambda-calculus-parser.em
The parser part of the lambda term expression parser.
utilities.em
A group of reasonably generic utilities used by the rest of the program.
test.pl
A test script written in Perl (not EuLisp so that any EuLisp errors affecting the code do not affect the test script in the same way).

All code is distributed under the terms of the GPL.