## Starter Files

Download lab14.zip. Inside the archive, you will find starter files for the questions in this lab, along with a copy of the OK autograder.

### Question 1: Remove All

Implement a function `remove_all` that takes a `Link`, and a `value`, and remove any list node containing that value. You can assume the list already has at least one node containing `value` and the first element is never removed. Notice that you are not returning anything, so you should mutate the list.

``````def remove_all(link , value):
"""Remove all the nodes containing value. Assume there exists some
nodes to be removed and the first element is never removed.

<0 2 2 3 1 2 3>
>>> remove_all(l1, 2)
<0 3 1 3>
>>> remove_all(l1, 3)
<0 1>
"""
return
else:

# alternate solution

Use OK to test your code:

``python3 ok -q remove_all``

## Trees

### Question 2: Reverse Other

Write a function `reverse_other` that mutates the tree such that every other (odd_indexed) level of the tree's entries are all reversed. For example `Tree(1,[Tree(2), Tree(3)])` becomes `Tree(1,[Tree(3), Tree(2)])`

``````def reverse_other(t):
"""Reverse the entries of every other level of the tree using mutation.

>>> t = Tree(1, [Tree(2), Tree(3), Tree(4)])
>>> reverse_other(t)
>>> t
Tree(1, [Tree(4), Tree(3), Tree(2)])
>>> t = Tree(1, [Tree(2, [Tree(5, [Tree(7), Tree(8)]), Tree(6)]), Tree(3)])
>>> reverse_other(t)
>>> t
Tree(1, [Tree(3, [Tree(5, [Tree(8), Tree(7)]), Tree(6)]), Tree(2)])
"""
def reverse_helper(t, need_reverse):
if t.is_leaf():
return
new_labs = [child.root for child in t.branches][::-1]
for i in range(len(t.branches)):
child = t.branches[i]
reverse_helper(child, not need_reverse)
if need_reverse:
child.root = new_labs[i]
reverse_helper(t, True)``````

Use OK to test your code:

``python3 ok -q reverse_other``

## Object-Oriented Programming

### Question 3: Mint

Complete the `Mint` and `Coin` classes so that the coins created by a mint have the correct year and worth.

• Each `Mint` instance has a `year` stamp. The `update` method sets the `year` stamp to the `current_year` class attribute of the `Mint` class.
• The `create` method takes a subclass of `Coin` and returns an instance of that class stamped with the `mint`'s year (which may be different from `Mint.current_year` if it has not been updated.)
• A `Coin`'s `worth` method returns the `cents` value of the coin plus one extra cent for each year of age beyond 50. A coin's age can be determined by subtracting the coin's year from the `current_year` class attribute of the `Mint` class.
``````class Mint:
"""A mint creates coins by stamping on years.

The update method sets the mint's stamp to Mint.current_year.

>>> mint = Mint()
>>> mint.year
2017
>>> dime = mint.create(Dime)
>>> dime.year
2017
>>> Mint.current_year = 2100  # Time passes
>>> nickel = mint.create(Nickel)
>>> nickel.year     # The mint has not updated its stamp yet
2017
>>> nickel.worth()  # 5 cents + (85 - 50 years)
38
>>> mint.update()   # The mint's year is updated to 2100
>>> Mint.current_year = 2175     # More time passes
>>> mint.create(Dime).worth()    # 10 cents + (75 - 50 years)
35
>>> Mint().create(Dime).worth()  # A new mint has the current year
10
>>> dime.worth()     # 10 cents + (160 - 50 years)
118
>>> Dime.cents = 20  # Upgrade all dimes!
>>> dime.worth()     # 20 cents + (160 - 50 years)
128

>>> m = Mint()
>>> n = m.create(Nickel)
>>> n.worth()
5
>>> n.year = 2017
>>> n.worth()
113
"""
current_year = 2017

def __init__(self):
self.update()

def create(self, kind):
return kind(self.year)
def update(self):
self.year = Mint.current_year
class Coin:
def __init__(self, year):
self.year = year

def worth(self):
return self.cents + max(0, Mint.current_year - self.year - 50)
class Nickel(Coin):
cents = 5

class Dime(Coin):
cents = 10``````

Use OK to test your code:

``python3 ok -q Mint``

## Scheme

Scheme questions are to be done in `lab14.scm`.

### Question 4: Accumulate

Fill in the definition for the procedure `accumulate`, which combines the first `n` natural numbers according to the following parameters:

1. `combiner`: a function of two arguments
2. `start`: a number with which to start combining
3. `n`: the number of natural numbers to combine
4. `term`: a function of one argument that computes the nth term of a sequence

For example, we can find the product of all the numbers from 1 to 5 by using the multiplication operator as the `combiner`, and starting our product at 1:

``````scm> (define (identity x) x)
scm> (accumulate * 1 5 identity)  ; 1 * 1 * 2 * 3 * 4 * 5
120``````

We can also find the sum of the squares of the same numbers by using the addition operator as the `combiner` and `square` as the `term`:

``````scm> (define (square x) (* x x))
scm> (accumulate + 0 5 square)  ; 0 + 1^2 + 2^2 + 3^2 + 4^2 + 5^2
55``````
``````(define (accumulate combiner start n term)
(if (= n 0)
start
'YOUR-CODE-HERE
(combiner (accumulate combiner
start
(- n 1)
term)
(term n))  )
)

;;; Tests
(define (identity x) x)
(accumulate * 1 5 identity)
; expect 120

(define (square x) (* x x))
(accumulate + 0 5 square)
; expect 55``````

Use OK to test your code:

``python3 ok -q accumulate``

### Question 5: How Many Dots?

Implement `how-many-dots`, which takes in a nested scheme list `s` and returns the number of dots that appear when it is displayed by the interpreter (not counting decimal points). You may assume that `s` is a nested list that contains only numbers.

Hints: A dot appears when the second element of a pair is not a well formed list. The procedures `pair?`, `null?`, and `number?` test whether a value is a pair, `nil`, or a number, respectively.

``````(define (how-many-dots s)
'YOUR-CODE-HERE
(if (not (pair? s))
0
(+ (if (and (not (pair? (cdr s)))
(not (null? (cdr s))))
1
0)
(how-many-dots (car s))
(how-many-dots (cdr s)))))

;;; Tests

(how-many-dots '(1 2 3))
; expect 0
(how-many-dots '(1 2 . 3))
; expect 1
(how-many-dots '((1 . 2) 3 . 4))
; expect 2
(how-many-dots '((((((1 . 2) . 3) . 4) . 5) . 6) . 7))
; expect 6
(how-many-dots '(1 . (2 . (3 . (4 . (5 . (6 . (7))))))))
; expect 0``````

Use OK to test your code:

``python3 ok -q how-many-dots``

### Question 6: Swap

Write a tail-recursive function `swap`, which takes in a Scheme list `s` and returns a new Scheme list where every two elements in `s` are swapped.

``````(define (swap s)
'YOUR-CODE-HERE
(define (helper sofar rest)
(cond ((null? rest) sofar)
((null? (cdr rest)) (append sofar (list (car rest))))
(else (helper (append sofar (list (car (cdr rest)) (car rest)))
(cdr (cdr rest))))))
(helper () s))

;;; Tests

(swap (list 1 2 3 4))
; expect (2 1 4 3)
(swap (list 1 2 3 4 5))
; expect (2 1 4 3 5)``````

Use OK to test your code:

``python3 ok -q swap``

## Interpreters

### Question 7: Eval/Apply

This question is from the final review.

With your Project 4 implementation, how many total calls to `scheme_eval` and `scheme_apply` would be made when evaluating the following two expressions? Assume that you are not using the tail call optimized `scheme_eval_optimized` function.

``````(define (square x) (* x x))
(+ (square 3) (- 3 2))``````
``14 eval calls, 4 apply calls``

## Iterators and Generators

### Question 8: Str

Write an iterator that takes a string as input and outputs the letters in order when iterated over.

``````class Str:
def __init__(self, str):
self.str = str
def __iter__(self):
return iter(self.str)``````

That works (why?), but just kidding.

``````class Str:
"""
>>> s = Str("hello")
>>> for char in s:
...     print(char)
...
h
e
l
l
o
>>> for char in s:    # a standard iterator does not restart
...     print(char)
"""
def __init__(self, str):
self.str = str
self.i = -1

def __iter__(self):
return self

def __next__(self):
self.i += 1
if self.i >= len(self.str):
raise StopIteration
return self.str[self.i]``````

Use OK to test your code:

``python3 ok -q Str``

### Question 9: List-Dictionary Iterator

Create an `ListDictIter` iterator class that is initialized with a list and dictionary. Calling next on an instance of the class will return `x`. `x` is the dictionary value of the next item of the list, or, if the item is not a key in the dictionary, `x` is just the item.

Hint: you can use the get method of a dictionary to specify a default value if a key is not in the dictionary, e.g. `dct.get("dog", "cat")` will return either the value that "dog" is a key for in dct, or "cat" if "dog" is not a key.

``````class ListDictIter:
""" Returns an iterator that yields either the corresponding
dictionary value of the next element in the list or the element
itself if it is not a key in the dictionary.

>>> sounds = {'cat': 'meow', 'cow': 'moo','ghost': 'boo'}
>>> things = ['cat', 'moo', 'ghost', 'pikachu']
>>> x = ListDictIter(things, sounds)
>>> list(x)
['meow', 'moo', 'boo', 'pikachu']
>>> y = ListDictIter(things, sounds)
>>> next(y)
'meow'
>>> next(y)
'moo'
"""

def __init__(self, lst, dct):
self.lst = lst
self.dct = dct
self.index = 0
def __iter__(self):
return self

def __next__(self):
if self.index >= len(self.lst):
raise StopIteration
else:
m = self.index
self.index += 1
return self.dct.get(self.lst[m], self.lst[m])``````

Use OK to test your code:

``python3 ok -q ListDictIter``

### Question 10: Pairs (generator)

Write a generator function `pairs` that takes a list and yields all the possible pairs of elements from that list.

``````def pairs(lst):
"""
>>> type(pairs([3, 4, 5]))
<class 'generator'>
>>> for x, y in pairs([3, 4, 5]):
...     print(x, y)
...
3 3
3 4
3 5
4 3
4 4
4 5
5 3
5 4
5 5
"""
for i in lst:
for j in lst:
yield i, j``````

Use OK to test your code:

``python3 ok -q pairs``

### Question 11: Pairs (iterator)

Now write an iterator that does the same thing. You are only allowed to use a linear amount of space - so computing a list of all of the possible pairs is not a valid answer. Notice how much harder it is - this is why generators are useful.

``````class PairsIterator:
"""
>>> for x, y in PairsIterator([3, 4, 5]):
...     print(x, y)
...
3 3
3 4
3 5
4 3
4 4
4 5
5 3
5 4
5 5
"""
def __init__(self, lst):
self.lst = lst
self.i = 0
self.j = 0
def __next__(self):
if self.i == len(self.lst):
raise StopIteration
result = (self.lst[self.i], self.lst[self.j])
if self.j == len(self.lst) - 1:
self.i += 1
self.j = 0
else:
self.j += 1
return result
def __iter__(self):
return self``````

Use OK to test your code:

``python3 ok -q PairsIterator``

## Streams

### Question 12: Cycle

Define a function `cycle` which takes in as input an ordinary Python list and returns a stream which just repeats that list over and over For example, `cycle(['a', 'b', 'c'])` is the stream containing elements `['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b', 'c', ...]`. If the input list is empty, output the empty stream; otherwise, the output stream should be infinite.

``````def cycle(list):
"""
Returns a Stream that cycles through each entry of the list.
>>> s = cycle(['a', 'b', 'c'])
>>> s.first
'a'
>>> s.rest.first
'b'
>>> s.rest.rest.rest.rest.first
'b'
>>> t = cycle([])
>>> t is Stream.empty
True
>>> isinstance(s, Stream)
True
>>> isinstance(s.rest.rest, Stream)
True
"""
if list == []:
return Stream.empty
return Stream(list[0], lambda: cycle(list[1:] + list[:1]))``````

Use OK to test your code:

``python3 ok -q cycle``

### Question 13: Run-Length Encoding

Run-length encoding is a very simple data compression technique, whereby runs of data are compressed and stored as a single value. A run is defined to be a contiguous sequence of the same number. For example, in the (finite) sequence

``1, 1, 1, 1, 1, 6, 6, 6, 6, 2, 5, 5, 5``

there are four runs: one each of 1, 6, 2, and 5. We can represent the same sequence as a sequence of tuples:

``(5, 1), (4, 6), (1, 2), (3, 5)``

Notice that the first element of each tuple is the number of times a particular number appears in a run, and the second element is the number in the run.

We will extend this idea to (possibly infinite) streams. Write a function called `rle` that takes in a stream of data, and returns a corresponding stream of tuples, which represents the run-length encoded version of the stream. It will also take in the maximum size of any given run (default 10) to prevent having to compress infinite runs.

``````def rle(s, max_run_length=10):
"""
>>> example_stream = lst_to_stream([1, 1, 1, 2, 3, 3])
>>> encoded_example = rle(example_stream)
>>> stream_to_list(encoded_example, 3)
[(3, 1), (1, 2), (2, 3)]
>>> shorter_encoded_example = rle(example_stream, 2)
>>> stream_to_list(shorter_encoded_example, 4)
[(2, 1), (1, 1), (1, 2), (2, 3)]
>>> ints = Stream(1, lambda: increment_stream(ints))
>>> encoded_naturals = rle(ints)
>>> stream_to_list(encoded_naturals, 3)
[(1, 1), (1, 2), (1, 3)]
>>> ones = Stream(1, lambda: ones)
>>> encoded_ones = rle(ones, max_run_length=3)
>>> stream_to_list(encoded_ones, 3)
[(3, 1), (3, 1), (3, 1)]
"""
if s == Stream.empty:
return s
first, s, count = s.first, s.rest, 1
while s != Stream.empty and s.first == first and count < max_run_length:
s, count = s.rest, count+1
return Stream((count, first), lambda: rle(s, max_run_length))``````

Use OK to test your code:

``python3 ok -q rle``