Introduction

This is a simple REPL shell for untyped lambda expressions. I wrote it mostly for playing around a little bit with the lambda calculus. Some parts of it are wildly inefficient, but it is fine for education purposes.

The shell can evaluate lambda expressions, assignments and macros. It is not fully tested yet, but you can help with that :).

Installation

Building from Source

You need two things to build the project from source: Cargo and a C compiler/linker like gcc or clang.

# Clone the source repository
git clone https://github.com/jzbor/lash.git
# Build release build with cargo
cargo build --release
# You can now find the binary in ./target/release
ls ./target/release

Running as Nix Flake

If you have Nix installed and Flakes enabled, installing lash should be as easy as:

nix profile install github:jzbor/lash

If you are using NixOS or home-manager you probably know how to install it best yourself.

You can also run the program without permanently installing it like so:

nix profile install github:jzbor/lash

The Language

The Lambda Shell is based on the untyped Lambda calculus. It is a simple system of term substitution.

Untyped Lambda Calculus

There are two basic notions in the Lambda calculus:

  • Abstractions: The idea is that we define a function by specifying which variable we want to be able to substitute. A very simple example is the identity function, which takes a term as parameter and maps it to itself: λx . x
  • Applications: This allows us to actually evaluate a function. We derive a new term by substituting the variable in the abstraction with the argument term: (λ x . x x) (y z)(y z) (y z)

The Lambda Shell allows you to use either λ or \ as lambda symbol for abstractions.

You are allowed to abbreviate multiple abstractions as one: \x . \y . x y = \x y . x y.

In batch mode (when processing .lsh files) you are required to end every statement with a semicolon (;). This is not necessary in interactive mode.

Assignments

In order to keep this readable the Lambda Shell allows you to name terms and reuse them. To assign a name to a term the := tag is used:

one := \f . \x . f x
true := \x . \y . y

These terms are referred to with their name until they get resolved explicitly or during reduction (see macros).

Macros

Macros allow you to modify Lambda terms at interpretation time. For example you might want to print out a term, resolve its named terms or reduce/normalize it. Macros are only evaluated once when the term is initially parsed, after that tey are just replaced with the terms they return.

Available Macros:

  • !cnormalize - like !normalize but shows number of reductions performed
  • !debug - prints out the argument term
  • !macros - prints available macros
  • !normalize - reduce the term until it cannot be reduced further (this is the so-called normal form)
  • !reduce - execute one reduction step on the argument term
  • !resolve - resolve named terms and church numerals
  • !time - time the macro execution of term inside
  • !vnormalize/!vreduce - like !normalize and !reduce, but prints the reduction steps

Examples

[λ] one := \f . \x . f x
one := \f . \x . f x

[λ] !resolve (one y)
(\f . \x . f x) y
[λ] one := \f . \x . f x
one := \f . \x . f x

[λ] !vnormalize (one (\y . y y) z)
one (\y . y y) z
(\x . (\y . y y) x) z
(\x . x x) z
z z

Directives

Directives are a mechanism to influence certain behaviours of the interpreter. They do not interact with Lambda terms directly, but can change how for example macros are evaluated.

A directive always has to be on its own line and starts with an @:

@echo "hello world"

Available directives

@echo "<string>"

The @echo directives prints out the string that is passed to it to stdout. Make sure to put the argument in parenthesis.

@include "<path>"

You can include files that are then directly evaluated. Directives in the included file might also have an effect on following code, so make sure to import files before actually setting important interpreter behavior.

@set <key> <value>

Set compiler behavior with this directive. Settings you can use are:

  • strategy normal|applicative - changes the reduction strategy (see Strategies)
  • numerals true|false - enable or disable church numerals

@usestd

There is a unstable standard library, which is a collection of a few useful terms. By running @usestd these get included as named terms.

You can find the terms in src/stdlib.rs.

Strategies

When reducing Lambda terms there are a few different strategies we can pick.

Applicative aka leftmost-innermost

This strategy always replaces inner terms first:

[λ] @set strategy applicative
@set strategy applicative

[λ] !reduce ((\x . (\y . y y) x) z)
(\x . x x) z

Normal aka leftmost-outermost

This strategy always replaces outer terms first:

[λ] @set strategy normal
@set strategy normal

[λ] !reduce ((\x . (\y . y y) x) z)
(\y . y y) z

Church Encodings

After Alonzo Church introduced the lambda calculus he also introduced means to represent different sorts of data with it.

Booleans

Booleans most often represent a truth value and can be either true or false. In church encodings they are represented by a function that takes two inputs and discards either the first or second value:

TRUE := λx . λy . x
FALSE := λx . λy . y

In lash the standard library gives definitions for TRUE and FALSE as well as several operations such as AND, OR and NOT.

Numerals

A positiv number n is encoded by applying a function f n times to an argument x:

0 := λf . λx . x
1 := λf . λx . f x
2 := λf . λx . f (f x)
3 := λf . λx . f (f (f x))
  .
  .
  .

In lash Church Numerals can be used by prefixing a demoniator with the $ sign. This has to be enabled either at interpreter invocation or at runtime. Operations are defined in the standard library.

Example:

[λ] @usestd
@usestd

[λ] @set numerals true
@set numerals true

[λ] !resolve $3
\f . \x . f (f (f x))

[λ] !normalize (ADD $1 $2)
\f . \x . f (f (f x))

See also

Further Reading