Haskell辅导 | COMP2209 Assignment Haskell Programming Challenges

这个作业是了解Haskell函数式编程的概念,并能够以这种风格编写程序,

COMP2209 Assignment Instructions
Learning Outcomes (LOs)
• Understand the concept of functional programming and be able to write programs in this style,
• Reason about evaluation mechanisms.
Introduction
This assignment asks you to tackle some functional programming challenges associated with
interpreting, translating, analysing and parsing variations of the lambda calculus, a language which is
known to be Turing complete1
. It is hoped these challenges will give you additional insights into the
mechanisms used to implement functional languages, as well as practising some advanced functional
programming techniques such as pattern matching over recursive data types, complex recursion, and
the use of monads in parsing. Your solutions need not be much longer than a page or two, but more
thought is required than with previous programming tasks you have worked on. Each challenge is
independent so that if you find one of them difficult you can move on to the next one.
For ease of comprehension, the examples in these instructions are given in a human readable format.
To assist with your implementation a file called Challenges2019tests.hs is provided with the sample
test cases but in machine readable format so that you do not need to re-type them. This test file
imports the Challenges.hs code you are asked to develop and submit. A dummy version of
Challenges.hs is also provided which you should edit to incorporate the code you have developed.
You may and indeed should define auxiliary or helper functions to ensure your code is easy to read
and understand. You must not, however, change the signatures of the functions which are exported
from Challenges.hs for testing, nor their defined types. Likewise you may not add any third party
Import statements, so that you may only Import modules from the standard Haskell distribution. If
you make such changes, your code may fail to compile on the server used for automatic marking, and
you will lose a significant number of marks.
As well as the published test cases an additional test suite will be applied to your code during marking.
To prevent anyone from gaining advantage from special case code, this second test suite will only be
published after marking has been completed.
It is your responsibility to adhere to the instructions specifying the behaviour of each function, and
your work will not receive full marks if you fail to do so. Your code will be tested only on values
satisfying the assumptions stated in the description of each challenge, so you can implement any error
handling you wish, including none at all. Where the specification allows more than one possible result,
any such result will be accepted. When applying the tests it is possible your code will run out of space
or time. A solution which fails to complete a test suite for one exercise within 30 seconds on the test
server will be deemed to have failed that exercise, and will only be eligible for partial credit. Any

1 Church showed how to encode integer arithmetic and Booleans as pure lambda expressions; combinators
which enable recursive definitions to be encoded were invented by Turing and Curry, among others.
reasonably efficient solution should take significantly less time than this to terminate on the actual
test data that will be supplied.
Depending on your proficiency with functional programming, the time required for you to implement
and test your code is expected to be 5 to 10 hours per challenge. If you are spending much longer
than this, you are advised to consult the teaching team for advice on coding practices.
Note that this assignment involves slightly more challenging programming compared to the previous
functional programming exercises. You may benefit, therefore, from the following advice on
debugging and testing Haskell code:
https://wiki.haskell.org/Debugging
https://www.quora.com/How-do-Haskell-programmers-debug
http://book.realworldhaskell.org/read/testing-and-quality-assurance.html
It is possible you will find samples of code on the web providing similar behaviour to these
challenges. Within reason, you may incorporate, adapt and extend such code in your own
implementation. Warning: where you make use of code from elsewhere, you must acknowledge and
cite the source(s) both in your code and in the bibliography of your report. Note also that you cannot
expect to gain full credit for code you did not write yourself, and that it remains your responsibility to
ensure the correctness of your solution with respect to these instructions.
Lambda Calculus and Let Expressions
You should start by reviewing the material on the lambda calculus given in lectures 12, 13 and 14. You
may also review the wikipedia article, https://en.wikipedia.org/wiki/Lambda_calculus, or Selinger’s
notes http://www.mscs.dal.ca/~selinger/papers/papers/lambdanotes.pdf, or both.
The simplest lambda notation uses a syntax such as λx -> e where x is a variable and e another lambda
expression. In this notation, the lambda expression has only one formal argument or
parameter. Application of a lambda function is written here the same way as in Haskell, with the
function followed by its actual argument, which is also known as juxtaposition. Lambda expressions
may be nested, for example: λx -> λy -> e.
You will be using the following data type for the abstract syntax of this notation:
data LamExpr =
LamApp LamExpr LamExpr | LamAbs Int LamExpr | LamVar Int deriving (Show, Eq)
It is assumed here that each variable is represented as an integer using some numbering scheme. For
example, it could be required that each variable is written using the letter x immediately followed by
a natural number, such as for example x0, x1, and x456. These are then represented in the abstract
syntax as Var 0, Var 1, and Var 456 respectively. Likewise, for example, the lambda expression λx1 ->
x2 x1 is represented as LamAbs 1 (LamApp (Var 2) (Var 1)). Representing variables using integers
rather than strings makes it is easier to generate a fresh variable that has not been already used
elsewhere.
The let notation can also be used to define a function but without the need for any Greek letter or
arrow. For example let f x = e, and let f x y = e are both let expressions that define a function f. A let
expression can also be applied, for example let f x = e in f y. Multiple let expressions can also be
defined simultaneously by separating them with a semi-colon, for example let f x = e; y = d in f y. To
make this notation seem more concrete, numeric literals are allowed in these expressions, albeit these
will also interpreted as just another shorthand for a lambda expression.
The let expressions used in this assignment have the following concrete syntax (expressed in BNF):
Expr ::= Num | Var | Fun | Expr Expr | “let” EqnList “in” Expr | “(“ Expr “)”
EqnList ::= Eqn | EqnList “;” Eqn
Eqn ::= Fun VarList “=” Expr
Fun ::= “f” Digits
VarList ::= Var | Var VarList
Var ::= “x” Digits
Num ::= Digits
Digits ::= Digit | Digits Digit
Digit ::= “0” | “1” | “2” | “3” | “4” | “5” | “6” | “7” | “8” | “9”
As with the lambda expressions above, variable and function names in these let expressions are
represented numerically (x0, x1, x2, … and f0, f1, f2, … respectively) to simplify processing.
You will be using the following data type for the abstract syntax of this notation:
data LetExpr =
LetApp LetExpr LetExpr | LetDef [([Int], LetExpr)] LetExpr | LetFun Int | LetVar Int |
LetNum Int deriving (Show, Eq)
It is assumed in these challenges that application associates to the left and binds tighter than any other
operator in both of the notations used here.
Functional Programming Challenges
Challenge 1: Converting a Lambda Calculus Expression to Alpha Normal Form
The first challenge requires you to write a function to convert a lambda expression to an alpha
equivalent one and return this value. In general, there would be many possible answers. For example,
λx0 -> λx1 -> x0 is alpha equivalent to λx1 -> λx -> y. Your function however must construct an answer
in which each bound variable uses the first available identifier. Note that variable numbering starts
at 0. The first lambda expression above is therefore in alpha normal form, according to the definition
given here, whereas the second is not. Care is required to avoid hiding a variable unintentionally. For
example, in the expression λx0 -> λx1 -> λx2 -> x0, you cannot rename x1 to x0 as this would bind the
later occurrence of x0 to the wrong lambda abstraction. On the other hand, you can safely rename x2
to x1 as x1. Your implementation should have the property that two alpha equivalent expressions have
the same alpha normal form. Here are some examples of this alpha normal form:
Lambda term Alpha Normal Form
x1 x0 x1 x0
λx3 -> x2 λx0 -> x2
λx0 -> λx1 -> x0 λx0 -> λx1 -> x0
λx1 -> λx0 -> x1 λx0 -> λx1 -> x0
λx1 -> λx0 -> x0 λx0 -> λx0 -> x0
λx0 -> λx1 -> λx2 -> x0 λx0 -> λx1 -> λx1 -> x0
Challenge 2: Counting Lambda Calculus Reductions
For this challenge you will program beta reduction of a lambda expression using all possible strategies.
In general a lambda expression may contain more than reducible (sub-)expression (redex). There are
therefore many different possible reduction sequences. We are interested here only in reduction
sequences that lead to an expression without any redexes, that is to say in beta normal form. The
famous Church-Rosser theorem states that any two such end results will be the same, or at least alpha
equivalent, lambda expressions. Note however that lambda reduction may not terminate, and
moreover that some reduction sequences will be longer than others.
For example, the lambda expression whose concrete syntax is shown below has two reducible
expressions, which are indicated by the underlined text: (λx -> λy -> x) z ((λt -> t) u). One reduction
sequence is therefore:
(λx -> λy -> x) z ((λt -> t) u)
(λy -> z) ((λt -> t) u)
z
and another one is:
(λx -> λy -> x) z ((λt -> t) u))
(λx -> λy -> x) z u
(λy -> z) u
z
You are asked to program a function which counts the number of different reduction sequences up to
a given maximum length, starting with the given lambda expression and ending with a beta normal
form that cannot be reduced further, and returns this count. Examples of this function are:
Lambda expression Maximum Count of All Different Reduction Sequences
λx -> (λy -> y) 0 0 – as no reduction sequence is this short
λx -> (λy -> y) 1 1 – one singleton sequence containing this
expression
λx -> (λy -> y) 2 1 – no other reduction sequences exist
(λx -> x)(λy -> y) 0 0
(λx -> x)(λy -> y) 1 0 – this expression is not in beta normal form yet
(λx -> x)(λy -> y) 2 1 – one reduction step gives a sequence of length 2
(λx -> λy -> x) z ((λt -> t) u) 2 0
(λx -> λy -> x) z ((λt -> t) u) 3 1
(λx -> λy -> x) z ((λt -> t) u) 4 3 – three reduction sequences of length up to 4 exist
Challenge 3: Pretty Printing a Lambda Expression
You are asked to write a “pretty printer” to display a simple lambda expression. Your output should
produce a syntactically correct expression. In addition, your solution should omit brackets where
these are not required. That is to say, omit brackets when the resulting string represents the same
abstract expression as the original one. Finally, your function should recognise a lambda expression
or sub-expression alpha equivalent to the Scott encoding of a natural number, and print the
corresponding numeral rather than the lambda expression. Use the encoding in which2
0 is represented by λx -> λy -> x
1 is represented by λx -> λy -> y (λx -> λy -> x)
2 is represented by λx -> λy -> y (λx -> λy -> y (λx -> λy -> x)), and so on.

2 You may find slight variations elsewhere in the literature, since Scott did not formally publish his encoding. See
the article Programming in the λ-calculus: from Church to Scott and back (Jansen 2013, The Beauty of Functional
Code, Springer) for further explanations and discussion of this encoding and others.
In your output, use the backslash character to stand for the λ symbol. Beyond that you are free to
format and lay out the expression as you choose in order to make it shorter or easier to read or both.
Some examples of pretty printing are:
LamApp (LamVar 2) (LamVar 1) “x2 x1”
LamApp (LamAbs 1 (LamVar 1)) (LamAbs 1 (LamVar 1)) “(λx1 -> x1) λx1 -> x1”
LamAbs 1 (LamApp (LamVar 1) (LamAbs 1 (LamVar 1))) “λx1 -> x1 λx1 -> x1”
LamAbs 1 (LamAbs 2 (LamVar 1)) “0”
LamAbs 1 (LamAbs 1 (LamApp (LamVar 1) (LamAbs 1
(LamAbs 2 (LamVar 1)))))
“1”
Challenge 4: Parsing Let Expressions
You are asked to write a parser for the let expressions defined by the BNF grammar above. You are
recommended to adapt the monadic parser examples published by Hutton and Meijer. You should
start by reading the monadic parser tutorial by Hutton in Chapter 13 of his Haskell textbook, or the
on-line tutorial below:
http://www.cs.nott.ac.uk/~pszgmh/pearl.pdf on-line tutorial
Your parser will have the type signature String -> Maybe LetExpr and will return Nothing if the given
string does not parse correctly according to the rules of the grammar. The grammar and LetExpr data
type are defined on page 3 of these instructions. Note that you will need to transform the grammar
into an equivalent form without left recursion, as this grammar construct results in a non-terminating
parser. Some examples of the parsing function are:
“let x1 = x2” Nothing — syntactically invalid wrt the given grammar
“x1 (x2 x3)” Just (LetApp (LetVar 1) (LetApp (LetVar 2) (LetVar 3)))
“x1 x2 x3” Just (LetApp (LetApp (LetVar 1) (LetVar 2)) (LetVar 3))
“let f1 x1 = x2 in f1 x1” Just (LetDef [([1,1], LetVar 2)] (LetApp (LetFun 1) (LetVar 1)))
“let f1 x2 = x2; f2 x1 = x1 in f1 x1” Just (LetDef [([1,2],LetVar 2),([2,1],LetVar 1)] (LetApp (LetFun 1)
(LetVar 1)))
Challenge 5: Converting a Let Expression to Lambda Calculus
The challenge requires you to convert a let expression into a lambda calculus expression. The lambda
expression will be represented using the following data type defined on page 2 of these instructions.
This challenge concerns parallel and recursive let definitions, so conversion can be quite complex. The
description below builds up step by step to the full algorithm.
First consider a simple let expression without any use of recursion or parameters
let f = e in d
Here any occurrence of f in d is to be replaced by e, which can be represented using the notation
d [ f := e ]
This substitution can also be expressed directly in the lambda calculus as
(λf -> d) e
The next step, involves a let expression including a parameter or parameters, for example
let f x y = e in d
This can be expressed as a simple let by defining f as an abbreviation for a lambda expression
let f = (λx -> λy -> e) in d
Hence by the simple let conversion rule above, the equivalent lambda expression is
(λf -> d) (λx -> λy -> e)
Note that this is equivalent, using the substitution notation above, to
d [ f := λx -> λy -> e ]
Now consider a recursive definition. Here it is recommended to use the trick described by Jansen
(2013, op. cit.) to replace the recursion with self-application3
let f x = e in d
Assume that f occurs free in d and e. This can nonetheless be reduced to a lambda expression via
let f’ = λf -> λx -> e[ f := f f] in
d [ f := f’ f’]
Using the approach described previously and eliminating the resulting substitutions will result in a
lambda calculus expression.
Finally, consider mutually recursive definitions of f and g.
let f x = e; g y = h; in d
These can be converted to
let f’ = λf -> λg -> λx -> e [ f := f f g || g := g f g ]
g’ = λf -> λg -> λy -> h [ f := f f g || g := g f g ]
in
d [ f := f’ f’ g’ || g := g’ f’ g’]
where || is a parallel substitution4
. Note that with care parallel substitution can be implemented by
a sequence of simple substitutions, resulting once more in a lambda calculus expression, or you may
choose to implement it directly. Note also that three or more mutually recursive functions can be
converted in a similar way.
Here are some examples of the letToLambda conversion function:
let f0 = f0 in f0 (λx0 -> x0 x0) λx0 -> x0 x0
let f1 x2 = x2 in f1 (λx0 λx0 -> x0) λx0 λx0 -> x0
let f1 x2 x3 = x3 x2 in f1 λx0 -> λx1 -> x1 x0
let f0 x0 = f1; f1 x1 = x1 in f0 λx0 -> λx1 -> x1
let f0 x0 x1 = x0; f1 x1 = f0 x1 f1 in f1 λx0 -> x0
Note that there are multiple possible answers, and that this table gives only one possibility in each
case. Your solution may produce different results and they will be marked correct so long as they are
beta equivalent to the lambda expressions above. (Some of the results above have been beta reduced
for brevity.)
Challenge 6: Converting Lambda Calculus to Let Expressions
This final challenge asks you to perform the reverse conversion by writing a function which takes a
lambda expression and returns an equivalent let expression. This quite easy to do if each lambda
abstraction can be converted locally. An additional constraint, however, is that the returned let
expression can have only one let definition block, which must occur at the topmost level. Hence all
functions defined in this block are global. There are various techniques you might use, such as the
ones found in the lambda calculus section of the Wikipedia article covering lambda lifting in different

3 An alternative is to use the Y fixed point combinator, which is also shown in Jansen’s paper. You may use this
solution if you prefer; it is more widely known and written about.
4 Parallel substitutions occur simultaneously. For example (a b)[a := b || b := a] simplifies to (b a). This
simplification can occur through a sequence of standard substitutions: (a b)[a := c][b := a][c := b].
notations: https://en.wikipedia.org/wiki/Lambda_lifting. Take care with your choice of individual
transformation steps to ensure that the overall conversion process works correctly.
Some examples of the lambdaToLet conversion function are:
λx0 -> x0 let f0 x0 = x0 in f0
x1 λx0 -> x0 let f0 x0 = x0 in x1 f0
(λx0 -> x0) x1 let f0 x0 = x0 in f0 x1
(λx0 -> x0) (λx0 -> x0) let f0 x0 = x0; f1 x0 = x0 in f0 f1
λx0 -> x0 λx1 -> x0 x1 let f0 x0 x1 = x0 x1; f1 x0 = x0 (f0 x0) in f1
As with the previous challenge, there are multiple correct answers. Your solution will be accepted as
passing the test provided its results are equivalent to those given above.
Implementation, Test File and Report
In addition to your solutions to these programming challenges, you are asked to submit an additional
Tests.hs file with your own tests, and a report.
You are expected to test your code carefully before submitting it. Your Tests.hs file may incorporate
and enhance the published Challenges2019tests.hs test cases and main function.
Your report should include an explanation of how you implemented and tested your solutions, which
should be up to 1 page (400 words). Note that this report is not expected to explain how your code
works, as this should be evident from your code itself together with any comments you have included
where necessary and appropriate. Instead you should cover the development and testing tools and
techniques you used, and comment on their effectiveness.
Your report should include a second page with a bibliography listing the source(s) for any fragments
of code written by other people that you have adapted or included directly in your submission.
Submission and Marking
Your Haskell solutions should be submitted as a single plain text file Challenges.hs. Your tests should
also be submitted as a plain text file Tests.hs. Finally, your report should be submitted as a PDF file,
Report.pdf.
The marking scheme is given in the appendix below. There are up to 5 marks for your solution to each
of the programming challenges, up to 5 for your explanation of how you implemented and tested
these, and up to 5 for your coding style. This gives a maximum of 40 marks for this assignment, which
is worth 40% of the module.
Your solutions to these challenges will be subjected to automated testing, so it is important you
adhere to the type definitions and type signatures given in the supplied dummy code file, and that
you do not change the list of functions and types exported by this file. Your code will be run using a
command line such as ghci –e “main” Challenges2019tests.hs, and you should check before you
submit that your solution compiles and runs as expected. The supplied Parsing.hs file will be present
so it is safe to Import this, and any library included in the standard Haskell distribution, but not third
party ones. Note that the comp2209 virtual machine is running the Glorious Glasgow Haskell
Compilation System version 7.6.3. Your own test cases will also be run using a similar command line,
so make sure these define a suitable main function which runs your tests and reports the results.
Appendix: Marking Scheme
Grade Functional Correctness Readability and Coding Style Development & Testing
Excellent
5 / 5
Your code passes the supplied
test cases and all the additional
tests we ran; anomalous inputs
are detected and handled
appropriately
You have clearly mastered this
programming language, libraries and
paradigm; your code is very easy to
read and understand; excellent
coding style
Proficient use of a range of
development & testing tools and
techniques correctly and
effectively to design, build and
test your software
Very Good
4 / 5
Your code passes the supplied
test cases and almost all the
additional tests we ran
Very good use of the language and
libraries; code is easy to understand
with very good programming style,
with only minor flaws
Very good use of a number of
development & testing tools and
techniques to design, build and
test your software
Good
3 / 5
Your code passes the supplied
test cases and some of the
additional tests we ran
Good use of the language and
libraries; code is mostly easy to
understand with good programming
style, some improvements possible
Good use of development &
testing tools and techniques to
design, build and test your
software
Acceptable
2 / 5
Your code passes some of the
supplied test cases; an
acceptable / borderline attempt
Acceptable use of the language and
libraries; programming style and
readability are borderline
Adequate use of development &
testing tools and techniques but
not showing full professional
competence
Poor
1 / 5
Your code compiles but does not
run; you have attempted to code
the required functionality
Poor use of the language and
libraries; coding style and readability
need significant improvement
Some use of development &
testing tools and techniques but
lacking professional competence
Inadequate
0 / 5
You have not submitted code
which compiles and runs; not a
serious attempt
Language and libraries have not
been used properly; expected
coding style is not used; code is
difficult to read
Inadequate use of development
tools and techniques; far from
professional competence
Guidance on Coding Style and Readability
Authorship You should include a comment in a standard format at the start of your code identifying you as the
author, and stating that this is copyright of the University of Southampton. Where you include any
fragments from another source, for example an on-line tutorial, you should identify where each of
these starts and ends using a similar style of comment.
Comments If any of your code is not self-documenting you should include an appropriate comment. Comments
should be clear, concise and easy to understand and follow a common commenting convention. They
should add to rather than repeat what is already clear from reading your code.
Variable and Function
Names
Names in your code should be carefully chosen to be clear and concise. Consider adopting the naming
conventions given in professional programming guidelines and adhering to these.
Ease of
Understanding and
Readability
It should be easy to read your program from top to bottom. This should be organised so that there is
a logical sequence of functions. Declarations should be placed where it is clear where and why they
are needed. Local definitions using let and where improve comprehensibility.
Logical clarity Functions should be coherent and clear. If it is possible to improve the re-usability of your code by
breaking a long block of code into smaller pieces, you should do so. On the other hand, if your code
consists of blocks which are too small, you may be able to improve its clarity by combining some of
these.
Maintainability Ensure that your code can easily be maintained. Adopt a standard convention for the layout and
format of your code so that it is clear where each statement and block begins and ends, and likewise
each comment. Where the programming language provides a standard way to implement some
feature, adopt this rather than a non-standard technique which is likely to be misunderstood and more
difficult to maintain. Avoid “magic numbers” by using named constants for these instead.
Andy Gravell, December 2019