3 A3: Let Bindings and Functions
Due: April 7, 2025
In the year 2142, the Intergalactic Data Science Consortium (IDSC) relies on the Lambda: First-class Functions language, a general-purpose language used across space stations and research outposts for analyzing extraterrestrial data. However, researchers have encountered major inefficiencies in their computations:
Lets can binding with one variable: Every time they process complex sensor readings, they must use a new let for binding each temporary variable, wasting valuable scientist time writing code.
No multi-argument functions: Every function can only take a single value, making it cumbersome to model relationships between multiple data points.
The goal of this assignment is to extend the Lambda language to support these features. We will call this language Lambda+. This will require you to change the language in a similar fashion as we have been doing in class.
You are given a file on Canvas hw3.rkt with a starter code for the assignment. This is same as the code for the Lambda: First-class Functions language from class. You should add your own test cases to ensure your submission is correct. You are tasked with extending the language to Lambda+ in the following ways:
Add the general form of let bindings
Add a let* form to the language
Add multi-argument functions and arity checking for function arguments
You have to submit one language implementation with all these changes, and not multiple languages.
3.1 General form of let bindings
Whenever users of the Lambda language need multiple bindings, they have to write the following:
instead, it would be nicer to collapse all the let-s into one as you would write in Racket:
The let bindings in our language are a simplification of the general form of let. The general form of let bindings allow you to declare multiple variables at once, like the example above.
The general form of let can bind identifiers id0 through idn to values of expressions e0 through en, locally for the expression e:
(let ((id0 e0) (id1 e1) ... (idn en)) e)
The meaning of a let expression (let ((id0 e0) ... (idn en)) e) is the meaning of e (the body of the let) when variables id0 through idn means the value of e0 through en respectively. The ids are bound “in parallel.” That is, no identifier is bound in the right-hand side expression for any id, but all are available in the body. The ids must be different from each other. Here is an example to explain this:
> (let ((x 1) (y (add1 x))) (+ x y)) x: undefined;
cannot reference an identifier before its definition
in module: 'program
The above results in an error, as declaring y requires the x to be defined, but all the newly declared bindings are only available in the body. Thus (+ x y) can be evaluated, but the second binding expression is invalid in this let form.
This let does not allow same identifier to be declared more than once. So programs like below result in an error:
> (let ((x 1) (x (add1 x))) (add1 x)) eval:144:0: let: duplicate identifier
at: x
in: (let ((x 1) (x (add1 x))) (add1 x))
Implement this general form of let.
3.2 Add let*
Just like let allows variables to be declared “in parallel”, the let* form allows you declare variables “in sequence”. Thus, programs like:
mean the same if they were written with let. However, let* supports latter declarations to be defined in terms of earlier declarations like:
The above program is valid, and evaluates to 3.
The general form of let* can bind identifiers id0 through idn to values of expressions e0 through en, locally for the expression e:
(let* ((id0 e0) (id1 e1) ... (idn en)) e)
The meaning of a let* expression (let* ((id0 e0) ... (idn en)) e) is the meaning of e (the body of the let*) when variables id0 through idn means the value of e0 through en respectively. The difference from let is that each identifier is available for use in later expressions, as well as in the body. Furthermore, the ids need not be distinct, and the most recent binding is the one that is used.
In the following example:
first x is bound to 1. This is used to compute (add1 x), i.e., 2 which is bound to the x again. Finally, this new value of x is used to compute the body (* x 2) which evaluates to 4.
Implement this form of let*.
3.3 Multi-argument functions and arity checking
Recall how we have to write multi-argument functions in the Lambda language:
defines adder that adds two numbers, by defining two lambdas to take two arguments. Your job for this task is to add native support in the language for multi-argument functions, so the scientists at IDSC can write programs easily:
If a function has n formal parameters, it can only be applied to n arguments. The number of arguments to a function is called its arity. If they mismatch it results in an error. Consider the example:
> (let ((adder (λ (x y) (+ x y)))) (adder 2)) adder: arity mismatch;
the expected number of arguments does not match the given
number
expected: 2
given: 1
will result in an error, as the function adder takes 2 arguments, but only 1 is provided. You should check for function arity when applying the function, i.e., check if the number of formal arguments and actual arguments are same and only then evaluate the body of the function. If they are different, raise an error stating arity mismatch.
3.4 Testing
You should test your code by writing test cases and adding them to relevant files. Use the command raco test hw3.rkt to test your code. Alternatively, pressing “Run” in Dr. Racket will also run your test cases. There are few test cases already included in hw3.rkt. These are by no means exhaustive, and you should add your own test cases to check your interpreter is correct.
For grading, your submitted interpreter will be tested on multiple programs drawn from this language. Writing your own test cases will give you confidence that your interpreter can handle previously unseen programs.
3.5 Submitting
You should submit on Gradescope. You should submit only one file: hw3.rkt for grading, so make sure all your work is contained there! You may add any function you need to these files, but do not rename the file or the interp function.