Skip to content

-Calculus Evaluator

This homework assignment aims to implement an evaluator of -expressions in Haskell, reducing a given -expression into its normal form following the normal order evaluation strategy. As a side effect, this homework assignment will also help you consolidate your knowledge of -calculus. The -expressions will be represented as instances of a suitably defined Haskell data type. So you will practice how to work with such types, in particular how to make it an instance of the Show class and how to use pattern matching with them.

The interpreter should be implemented as a Haskell module called Hw3. Note the capital H. The module names in Haskell have to start with capital letters. As the file containing the module code must be of the same name as the module name, all your code is required to be in a single file called Hw3.hs. Your file Hw3.hs has to start with the following lines:

haskell
module Hw3 where

type Symbol = String
data Expr = Var Symbol
          | App Expr Expr
          | Lambda Symbol Expr deriving Eq

The first line defines a module of the name Hw3. The names of variables in -terms are represented by instances of String. The second line just introduces a new name Symbol for the type String to distinguish visually when we deal with the variable names. The last line defines the data type representing -expressions. There are three data constructors: one for a variable, one for an application and one for -abstraction. The clause deriving Eq makes the data type instance of the Eq class so that it is possible to check whether two -expressions are equal or not.

Evaluator specification

First, make the data type Expr an instance of the Show class so that ghci can display -expressions. So you need to define the show function converting -expressions into a string. Once you type into the ghci prompt the following expressions, it should behave as follows:

haskell
λ> Var "x"
x

λ> App (Var "x") (Var "y")
(x y)

λ> Lambda "x" (Var "x")
(\x.x)

λ> App (Lambda "x" (Var "x")) (Var "y")
((\x.x) y)

λ> Lambda "s" (Lambda "z" (App (Var "s") (App (Var "s") (Var "z"))))
(\s.(\z.(s (s z))))

So the symbol is displayed as \. Variables are displayed as their names. Applications are displayed as (e1 e2) with a space separating expressions e1,e2 and -abstractions as (\x.e).

As a next step, your task is to implement a function eval :: Expr -> Expr. This function for a given input -expression returns its normal form if it exists. Moreover, it has to follow the normal order evaluation strategy. So to make a single step -reduction, you have to identify the leftmost outermost redex and reduce it. Then you repeat this process until there is no redex.

To reduce a redex, you have to implement the substitution mechanism allowing you to substitute a -expression for all the free occurrences of a variable in another -expression. This mechanism has to deal with name conflicts, as you know from the lecture on -calculus. One possibility how to safely do that is the following recursive definition:

The last case deals with the name conflicts, i.e., it uses -conversion. As is free in in this case, it could become bound after the substitution in . Thus we rename in to a new fresh variable , i.e., we compute and then replace the variable in the -abstraction to . Then we can continue and recursively substitute for in . So follow the above recursive definition in your code.

Your code has to generate fresh symbols when needed. The fresh symbols can be denoted, e.g., "a0","a1","a2",.... To generate a fresh symbol, it suffices to increment the number of the last used symbol. This number is a state of your computation. As Haskell is a purely functional language, you cannot have a state and update it when necessary. Instead, you need to include the index into signatures of your functions similarly, as we did in the exercise from Lab 9 where we implemented a function indexing nodes of a binary tree.

Test cases

Below you find several public test cases. If the -expression is already in its normal form, the eval function just returns its input.

haskell
λ> eval (App (Var "x") (Var "y"))
(x y)
λ> eval (Lambda "x" (Var "x"))
(\x.x)

If it is reducible, it returns its normal form. For instance is reduced to :

haskell
λ> eval (App (Lambda "x" (Var "x")) (Var "y"))
y

Consider the expression . The reduction gives . By the definition of substitution, we have to rename to and then substitute for the free occurrences of , i.e.,

haskell
λ> eval (App (Lambda "x" (Lambda "y" (App (Var "x") (Var "y")))) (Var "y"))
(\a0.(y a0))

Consider the -expression . The reduction leads to . As is free, we introduce by the definition a fresh variable instead of obtaining . Then we compute .

haskell
λ> eval (App (Lambda "x" (Lambda "y" (Var "y"))) (Var "y"))
(\a0.a0)

Consider the -expression . As and are free in we have to rename them in obtaining .

haskell
ex = App (Lambda "x"
           (Lambda "y"
             (Lambda "z" (App (App (Var "x") (Var "y")) (Var "z")))))
         (App (Var "y") (Var "z"))

λ> eval ex
(\a0.(\a1.(((y z) a0) a1)))

To write more complex test cases, you can define subexpressions and then compose more complex ones. For instance, to test that reduces to :

haskell
one = Lambda "s" (Lambda "z" (App (Var "s") (Var "z")))
suc = Lambda "w"
       (Lambda "y"
         (Lambda "x"
           (App (Var "y")
                (App (App (Var "w") (Var "y"))
                     (Var "x")))))

λ> eval (App suc one)
(\y.(\x.(y (y x))))

One more test case using the definition . Consider the -expression

It reduces to . As is free in , the bound occurrence of in is renamed. But is not renamed because by the defition of substitution we have .

haskell
λ> eval (App (Lambda "z" one) (App (Var "s") (Var "z")))
(\a0.(\z.(a0 z)))