Lab 9 Solutions

Solution Files

Lab Checkoff Questions

  • What parts of Scheme are similar to what we learned in Python?
  • It is a functional programming language that is recursive-based, lexical scoped, and all lists are linked lists!
  • What are the different ways you can create a list in scheme?
  • cons chains, list procedures, quote ' ` for creation; append, map, reduce, and filter are useful for list transformation/derivations.
  • What are some resources you can refer to if you ever need help on Scheme?
  • Refer to the Scheme Specification/Built-Ins under the Resources tab!

Attendance

You need to submit the lab problems in addition to attending to get credit for lab.

If you miss lab for a good reason (such as sickness or a scheduling conflict) or you don't get checked in for some reason, email cs61a@berkeley.edu within one week to receive attendance credit.

Scheme

Scheme is a famous functional programming language from the 1970s. It is a dialect of Lisp (which stands for LISt Processing). The syntax of Scheme is very unique: it involves prefix notation and many nested parentheses (see http://xkcd.com/297/). Scheme features first-class functions and optimized tail-recursion, which were fairly new when Scheme was introduced.

The Scheme specification and built-in procedure references are a good reference for scheme syntax.


If you choose to use VS Code as your text editor (instead of the web-based editor), install the vscode-scheme extension so that parentheses are highlighted.

Before:

After:


Consult the drop-downs below if you need a refresher on Scheme/Scheme Lists. It's okay to skip directly to the questions and refer back here should you get stuck.

Atomic expressions (also called atoms) are expressions without sub-expressions, such as numbers, boolean values, and symbols.

scm> 1234    ; integer
1234
scm> 123.4   ; real number
123.4
scm> #f      ; the Scheme equivalent of False in Python
#f

A Scheme symbol is equivalent to a Python name. A symbol evaluates to the value bound to that symbol in the current environment. (They are called symbols rather than names because they include + and other arithmetic symbols.)

scm> quotient      ; A symbol bound to a built-in procedure
#[quotient]
scm> +             ; A symbol bound to a built-in procedure
#[+]

In Scheme, all values except #f (equivalent to False in Python) are true values (unlike Python, which has other false values, such as 0).

scm> #t
#t
scm> #f
#f

All non-primitive expressions in Scheme are known as combinations and have the following syntax:

(<operator> <operand1> <operand2> ...)

Combinations are expressions that combine multiple expressions. Here, <operator>, <operand1>, and <operand2>. are all expressions. The number of operands depends on the operator. There are two types of combinations:

  1. A call expression, whose operator evaluates to a procedure
  2. A special form expression, whose operator is a special form

Scheme uses Polish prefix notation, in which the operator expression comes before the operand expressions. For example, to evaluate 3 * (4 + 2), we write:

scm> (* 3 (+ 4 2))
18

Just like in Python, to evaluate a call expression:

  1. Evaluate the operator. It should evaluate to a procedure.
  2. Evaluate the operands, left to right.
  3. Apply the procedure to the evaluated operands.

Here are some examples using built-in procedures:

scm> (+ 1 2)
3
scm> (- 10 (/ 6 2))
7
scm> (modulo 35 4)
3
scm> (even? (quotient 45 2))
#t

Define: The define form is used to assign values to symbols. It has the following syntax:

(define <symbol> <expression>)
scm> (define pi (+ 3 0.14))
pi
scm> pi
3.14

To evaluate the define expression:

  1. Evaluate the final sub-expression (<expression>), which in this case evaluates to 3.14.
  2. Bind that value to the symbol (symbol), which in this case is pi.
  3. Return the symbol.

The define form can also define new procedures, described in the "Defining Functions" section.

If Expressions: The if special form evaluates one of two expressions based on a predicate.

(if <predicate> <if-true> <if-false>)

The rules for evaluating an if special form expression are as follows:

  1. Evaluate the <predicate>.
  2. If the <predicate> evaluates to a true value (anything but #f), evaluate and return the value of the <if-true> expression. Otherwise, evaluate and return the value of the <if-false> expression.

For example, this expression does not error and evaluates to 5, even though the sub-expression (/ 1 (- x 3)) would error if evaluated.

scm> (define x 3)
x
scm> (if (> (- x 3) 0) (/ 1 (- x 3)) (+ x 2))
5

The <if-false> expression is optional.

scm> (if (= x 3) (print x))
3

Let's compare a Scheme if expression with a Python if statement:

  • In Scheme:
(if (> x 3) 1 2)
  • In Python:
if x > 3:
    1
else:
    2

The Scheme if expression evaluates to a number (either 1 or 2, depending on x). The Python statement does not evaluate to anything, and so the 1 and 2 cannot be used or accessed.

Another difference between the two is that it's possible to add more lines of code into the suites of the Python if statement, while a Scheme if expression expects just a single expression in each of the <if-true> and <if-false> positions.

One final difference is that in Scheme, you cannot write elif clauses.

Cond Expressions: The cond special form can include multiple predicates (like if/elif in Python):

(cond
    (<p1> <e1>)
    (<p2> <e2>)
    ...
    (<pn> <en>)
    (else <else-expression>))

The first expression in each clause is a predicate. The second expression in the clause is the return expression corresponding to its predicate. The else clause is optional; its <else-expression> is the return expression if none of the predicates are true.

The rules of evaluation are as follows:

  1. Evaluate the predicates <p1>, <p2>, ..., <pn> in order until one evaluates to a true value (anything but #f).
  2. Evalaute and return the value of the return expression corresponding to the first predicate expression with a true value.
  3. If none of the predicates evaluate to true values and there is an else clause, evaluate and return <else-expression>.

For example, this cond expression returns the nearest multiple of 3 to x:

scm> (define x 5)
x
scm> (cond ((= (modulo x 3) 0) x)
            ((= (modulo x 3) 1) (- x 1))
            ((= (modulo x 3) 2) (+ x 1)))
6


Lambdas: The lambda special form creates a procedure.

(lambda (<param1> <param2> ...) <body>)

This expression will create and return a procedure with the given formal parameters and body, similar to a lambda expression in Python.

scm> (lambda (x y) (+ x y))        ; Returns a lambda procedure, but doesn't assign it to a name
(lambda (x y) (+ x y))
scm> ((lambda (x y) (+ x y)) 3 4)  ; Create and call a lambda procedure in one line
7

Here are equivalent expressions in Python:

>>> lambda x, y: x + y
<function <lambda> at ...>
>>> (lambda x, y: x + y)(3, 4)
7

The <body> may contain multiple expressions. A scheme procedure returns the value of the last expression in its body.

The define form can create a procedure and give it a name:

(define (<symbol> <param1> <param2> ...) <body>)

For example, this is how we would define the double procedure:

scm> (define (double x) (* x 2))
double
scm> (double 3)
6

Here's an example with three arguments:

scm> (define (add-then-mul x y z)
        (* (+ x y) z))
scm> (add-then-mul 3 4 5)
35

When a define expression is evaluated, the following occurs:

  1. Create a procedure with the given parameters and <body>.
  2. Bind the procedure to the <symbol> in the current frame.
  3. Return the <symbol>.

The following two expressions are equivalent:

scm> (define add (lambda (x y) (+ x y)))
add
scm> (define (add x y) (+ x y))
add

As you read through this section, it may be difficult to understand the differences between the various representations of Scheme containers. We recommend that you use our online Scheme interpreter to see the box-and-pointer diagrams of pairs and lists that you're having a hard time visualizing! (Use the command (autodraw) to toggle the automatic drawing of diagrams.)

Lists

Scheme lists are very similar to the linked lists we've been working with in Python. Just like how a linked list is constructed of a series of Link objects, a Scheme list is constructed with a series of pairs, which are created with the constructor cons.

Scheme lists require that the cdr is either another list or nil, an empty list. A list is displayed in the interpreter as a sequence of values (similar to the __str__ representation of a Link object). For example,

scm> (cons 1 (cons 2 (cons 3 nil)))
(1 2 3)

Here, we've ensured that the second argument of each cons expression is another cons expression or nil.

list

We can retrieve values from our list with the car and cdr procedures, which now work similarly to the Python Link's first and rest attributes. (Curious about where these weird names come from? Check out their etymology.)

scm> (define a (cons 1 (cons 2 (cons 3 nil))))  ; Assign the list to the name a
a
scm> a
(1 2 3)
scm> (car a)
1
scm> (cdr a)
(2 3)
scm> (car (cdr (cdr a)))
3

If you do not pass in a pair or nil as the second argument to cons, it will error:

scm> (cons 1 2)
Error

list Procedure

There are a few other ways to create lists. The list procedure takes in an arbitrary number of arguments and constructs a list with the values of these arguments:

scm> (list 1 2 3)
(1 2 3)
scm> (list 1 (list 2 3) 4)
(1 (2 3) 4)
scm> (list (cons 1 (cons 2 nil)) 3 4)
((1 2) 3 4)

Note that all of the operands in this expression are evaluated before being put into the resulting list.

Quote Form

We can also use the quote form to create a list, which will construct the exact list that is given. Unlike with the list procedure, the argument to ' is not evaluated.

scm> '(1 2 3)
(1 2 3)
scm> '(cons 1 2)           ; Argument to quote is not evaluated
(cons 1 2)
scm> '(1 (2 3 4))
(1 (2 3 4))

Built-In Procedures for Lists

There are a few other built-in procedures in Scheme that are used for lists. Try them out in the interpreter!

scm> (null? nil)                ; Checks if a value is the empty list
True
scm> (append '(1 2 3) '(4 5 6)) ; Concatenates two lists
(1 2 3 4 5 6)
scm> (length '(1 2 3 4 5))      ; Returns the number of elements in a list
5
scm> (list-ref '(1 2 3 4 5) 2)  ; Returns the element at a given index (0-indexed)
3
scm> (car '(1 2 3))             ; Returns the first element of a list
1
scm> (cdr '(1 2 3))             ; Returns all elements after the first
(2 3)
scm> (reverse '(1 2 3 4 5))     ; Reverses the order of a list
(5 4 3 2 1)

Higher-Order Procedures for Lists

map, filter, and reduce let you transform or process lists without writing explicit recursion.

scm> (map (lambda (x) (* x x)) '(1 2 3 4)) ; Applies a procedure to every element
(1 4 9 16)
scm> (filter (lambda (x) (> x 2)) '(1 2 3 4)) ; Keeps only elements that satisfy a predicate
(3 4)
scm> (reduce + '(1 2 3 4))      ; Combines all elements using a procedure, left to right
10

A useful way to think about the difference:

  • map — Transforms each element, keeping the same number of elements. It's like you walk down the list and replace each item with (fn item).
  • filter — Keeps or drops each element, but never changes. Tests a yes/no predicate, if yes it stays as it is and if no it is dropped.
  • reduce — Collapses the whole list down to a single value (with a left to right combining procedure)

These three can often replace what would otherwise be a recursive function with a base case and recursive case. It may be useful for you to see what one of these might actually look like manually written out:

Writing map manually:

scm> (define (my-map fn lst)
       (if (null? lst)
           nil
           (cons (fn (car lst)) (my-map fn (cdr lst)))))
scm> (my-map (lambda (x) (* x x)) '(1 2 3 4))
(1 4 9 16)

Looking at this, these three higher-order procedures are just common recursive patterns that Scheme has built in for you. map for example just does the cons, cdr, and base-case work for you!

Required Questions

Scheme

Q1: Over or Under

Define a procedure over-or-under which takes in a number num1 and a number num2 and returns the following:

  • -1 if num1 is less than num2
  • 0 if num1 is equal to num2
  • 1 if num1 is greater than num2

Note: Remember that every parenthesis in Scheme makes a function call. For example, just typing 0 in the Scheme interpeter will return 0. However, typing (0) will cause an Error because 0 is not a function.

Challenge: Implement this in 2 different ways using if and cond!

(define (over-or-under num1 num2)
(cond ((< num1 num2) -1) ((= num1 num2) 0) (else 1))
)

Use Ok to test your code:

python3 ok -q over_or_under

Q2: Compose

Write the procedure composed, which takes in procedures f and g and returns a new procedure. This new procedure takes in a number x and returns the result of calling f on g of x.

Note: Remember to use Scheme syntax when calling functions. The form is (func arg), not func(arg).

(define (composed f g)
(lambda (x) (f (g x)))
)

Use Ok to test your code:

python3 ok -q composed

Q3: Repeat

Write the procedure repeat, which takes in a procedure f and a number n, and outputs a new procedure. This new procedure takes in a number x and returns the result of calling f on x a total of n times. For example:

scm> (define (square x) (* x x))
square
scm> ((repeat square 2) 5) ; (square (square 5))
625
scm> ((repeat square 3) 3) ; (square (square (square 3)))
6561
scm> ((repeat square 1) 7) ; (square 7)
49

Hint: The composed function you wrote in the previous problem might be useful.

(define (repeat f n)
; note: this relies on `composed` being implemented correctly (if (< n 1) (lambda (x) x) (composed f (repeat f (- n 1))))
)

Use Ok to test your code:

python3 ok -q repeat

Scheme Lists

Q4: WWSD: Lists

Use Ok to test your knowledge with the following "What Would Scheme Display?" questions:

python3 ok -q wwsd_lists -u
scm> (cons 1 (cons 2 nil))
(1 2)
scm> (car (cons 1 (cons 2 nil)))
1
scm> (cdr (cons 1 (cons 2 nil)))
(2)
scm> (list 1 2 3)
(1 2 3)
scm> '(1 2 3)
(1 2 3)
scm> (cons 1 '(list 2 3))  ; Recall quoting
(1 list 2 3)
scm> (cons 1 `(list 2 3)) ; Quasiquotes also work as quotes!
(1 list 2 3)
scm> '(cons 4 (cons (cons 6 8) ()))
(cons 4 (cons (cons 6 8) ()))
scm> (cons 1 (list (cons 3 nil) 4 5))
(1 (3) 4 5)

Q5: Make a List

In this problem you will create the list with the following box-and-pointer diagram:

linked list

Challenge: try to create this list in multiple ways, and using multiple list constructors!

(define lst
(cons (cons 1 nil) (cons 2 (cons (cons 3 (cons 4 nil)) (cons 5 nil))))
)

Use Ok to unlock and test your code:

python3 ok -q make_structure -u
python3 ok -q make_structure

Q6: Without Duplicates

Implement without-duplicates, which takes a list of numbers lst as input and returns a list that has all of the unique elements of lst in the order that they first appear, but without duplicates. For example, (without-duplicates (list 5 4 5 4 2 2)) evaluates to (5 4 2).

Hints: To test if two numbers are equal, use the = procedure. To test if two numbers are not equal, use the not procedure in combination with =. You may find it helpful to use the filter procedure with a helper lambda function to use as a filter.

(define (without-duplicates lst)
(if (null? lst) lst (cons (car lst) (without-duplicates (filter (lambda (x) (not (= (car lst) x))) (cdr lst)))))
)

Use Ok to unlock and test your code:

python3 ok -q without_duplicates -u
python3 ok -q without_duplicates

Check Your Score Locally

You can locally check your score on each question of this assignment by running

python3 ok --score

This does NOT submit the assignment! When you are satisfied with your score, submit the assignment to Gradescope to receive credit for it.

Submit Assignment

Submit this assignment by uploading any files you've edited to the appropriate Gradescope assignment. Lab 00 has detailed instructions.

Correctly completing all questions is worth one point. Please ensure your TA has taken your attendance before leaving.