Due at 11:59pm on 10/16/2016.

Starter Files

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

Submission

By the end of this lab, you should have submitted the lab using ok. You may submit more than once before the deadline; only the final submission will be graded.

  • All questions for this lab are required.

Data Abstraction

Data abstraction is a powerful concept in computer science that allows programmers to treat code as objects --- for example, car objects, chair objects, people objects, etc. That way, programmers don't have to worry about how code is implemented --- they just have to know what it does.

Data abstraction mimics how we think about the world. For example, when you want to drive a car, you don't need to know how the engine was built or what kind of material the tires are made of. You just have to know how to turn the wheel and press the gas pedal.

An abstract data type consists of two types of functions:

  • Constructors: functions that build the abstract data type.
  • Selectors: functions that retrieve information from the data type.

For example, say we have an abstract data type called city. This city object will hold the city's name, and its latitude and longitude. To create a city object, you'd use a constructor like

city = make_city(name, lat, lon)

To extract the information of a city object, you would use the selectors like

get_name(city)
get_lat(city)
get_lon(city)

For example, here is how we would use the make_city constructor to create a city object to represent Berkeley and the selectors to access its information.

>>> berkeley = make_city('Berkeley', 122, 37)
>>> get_name(berkeley)
'Berkeley'
>>> get_lat(berkeley)
122
>>> get_lon(berkeley)
37

The following code will compute the distance between two city objects:

from math import sqrt
def distance(city_1, city_2):
    lat_1, lon_1 = get_lat(city_1), get_lon(city_1)
    lat_2, lon_2 = get_lat(city_2), get_lon(city_2)
    return sqrt((lat_1 - lat_2)**2 + (lon_1 - lon_2)**2)

Notice that we don't need to know how these functions were implemented. We are assuming that someone else has defined them for us.

It's okay if the end user doesn't know how functions were implemented. However, the functions still have to be defined by someone. We'll look into defining the constructors and selectors later in this discussion.

Question 1: Closer city

Implement closer_city, a function that takes a latitude, longitude, and two cities, and returns the name of the city that is relatively closer to the provided latitude and longitude.

You may only use selectors and constructors (introduced above) for this question. You may also use the distance function defined above. All of these functions can be found in utils.py, if you are curious how they are implemented. However, the point of data abstraction, as we've said, is that we do not need to know how an abstract data type is implemented, but rather just how we can interact with and use the data type.

def closer_city(lat, lon, city1, city2):
    """ Returns the name of either city1 or city2, whichever is closest
        to coordinate (lat, lon).

        >>> berkeley = make_city('Berkeley', 37.87, 112.26)
        >>> stanford = make_city('Stanford', 34.05, 118.25)
        >>> closer_city(38.33, 121.44, berkeley, stanford)
        'Stanford'
        >>> bucharest = make_city('Bucharest', 44.43, 26.10)
        >>> vienna = make_city('Vienna', 48.20, 16.37)
        >>> closer_city(41.29, 174.78, bucharest, vienna)
        'Bucharest'
    """
"*** YOUR CODE HERE ***"
new_city = make_city('arb', lat, lon) dist1 = distance(city1, new_city) dist2 = distance(city2, new_city) if dist1 < dist2: return get_name(city1) return get_name(city2)

Use OK to test your code:

python ok -q closer_city --local

ADT: Trees

A tree is a data structure that represents a hierarchy of information. A file system is a good example of a tree structure. For example, within your cs88 folder, you have folders separating your projects, lab assignments, and homework. The next level is folders that separate different assignments, hw01, lab01, cuke, etc., and inside those are the files themselves, including the starter files and ok. Below is an incomplete diagram of what your cs88 directory might look like.

cs88 tree

As you can see, unlike trees in nature, which is where this data structure gets its name from, CS trees are drawn with the root at the top and the leaves at the bottom.

  • node: a single unit in a tree.
  • root: the node at the top of a tree; every tree has one root node.
  • branch: a child of a larger tree; has its own root and possibly branches of its own.
  • subtree: a descendant of a larger tree; a subtree is either a branch or a subtree of a branch of a larger tree.
  • leaf: a node that has no branches.

Our tree abstract data type consists of a root node and a list of its branches. To create a tree and access its root and branches, use the following constructor and selectors:

  • Constructor

    • tree(root, branches=[]): creates a tree object with the given root and list of branches.
  • Selectors

    • root(tree): returns the value of the root of the tree.
    • branches(tree): returns the list of branches of the given tree.
    • is_leaf(tree): returns True if tree's list of branches is empty, and False otherwise.

For example, the tree generated by

t = tree(1, [tree(2),
             tree(3, [tree(4), tree(5)]),
             tree(6, [tree(7)])])

would look like this:

   1
 / | \
2  3  6
  / \  \
 4   5  7

It may be easier to visualize this translation by formatting the code like this:

t = tree(1,
         [tree(2),
          tree(3,
               [tree(4),
                tree(5)]),
          tree(6,
               [tree(7)])])

To extract the number 3 from this tree, which is the value of the root of its second branch, we would do this:

root(branches(t)[1])

Question 2: Map

Define the function tree_map, which takes in a tree and a one-argument function as arguments and returns a new tree which is the result of mapping the function over the entries of the input tree.

def tree_map(fn, t):
    """Maps the function fn over the entries of tree and returns the
    result in a new tree.

    >>> numbers = tree(1, 
    ...                    [tree(2, 
    ...                          [tree(3),   
    ...                           tree(4)]),  
    ...                     tree(5,   
    ...                           [tree(6,                           
    ...                                [tree(7)]), 
    ...                           tree(8)])])
    >>> print_tree(tree_map(lambda x: 2**x, numbers))
    2
      4
        8
        16
      32
        64
          128
        256
    """
"*** YOUR CODE HERE ***"
if branches(t) == []: return tree(fn(root(t)), []) mapped_branches = [] for branch in branches(t): mapped_branches += [ tree_map(fn, branch) ] return tree(fn(root(t)), mapped_branches) # Alternate solution def tree_map(fn, t): return tree(fn(root(t)), [tree_map(fn, t) for t in branches(t)])

Use OK to test your code:

python ok -q tree_map --local

ADT: Rational Numbers

Using Abstractions

One day, you and a friend decide to work on a programming project — implement a Rational Number interface. Recall that a rational number is any number that can be expressed as p / q, where p and q are integers.

Your friend gets to work implementing an abstract data type (ADT for short) to represent rational numbers and comes up with the following constructors and selectors.

def make_rat(num, den):
    """Creates a rational number, given a numerator and
    denominator.
    """
    return lambda x, y: [lambda: den + x, lambda: num + y]

def num(rat):
    """Extracts the numerator from a rational number."""
    return rat(2, 3)[1]() - 3

def den(rat):
    """Extracts the denominator from a rational number."""
    return rat(8, 5)[0]() - 8

The code your friend wrote will work, but it is very hard to understand. Luckily, you respected data abstraction, so your own code doesn't depend on how your friend's works. You can write all your functions without having to worry about how exactly your friend's code works — that is the power of abstraction!

Question 3

Implement addition and subtraction for rational numbers (remember that you can use your solution for add_rat in sub_rat):

def add_rat(a, b):
    """Adds two rational numbers A and B. For example,
    (3 / 4) + (5 / 3) = (29 / 12)

    >>> a, b = make_rat(3, 4), make_rat(5, 3)
    >>> c = add_rat(a, b)
    >>> num(c)
    29
    >>> den(c)
    12
    """
"*** YOUR CODE HERE ***"
new_num = num(a) * den(b) + num(b) * den(a) new_den = den(a) * den(b) return make_rat(new_num, new_den)
def sub_rat(a, b): """Subtracts two rational numbers A and B. For example, (3 / 4) - (5 / 3) = (-11 / 12) >>> a, b = make_rat(3, 4), make_rat(5, 3) >>> c = sub_rat(a, b) >>> num(c) -11 >>> den(c) 12 """
"*** YOUR CODE HERE ***"
neg = make_rat(-num(b), den(b)) return add_rat(a, neg) # alternate solution def sub_rat_alternate(a, b): new_num = num(a) * den(b) - num(b) * den(a) new_den = den(a) * den(b) return make_rat(new_num, new_den)

Use OK to test your code:

python ok -q add_rat --local
python ok -q sub_rat --local

Question 4

Implement multiplication and division for rational numbers:

def mul_rat(a, b):
    """Multiplies two rational numbers A and B. For example,
    (3 / 4) * (5 / 3) = (15 / 12)

    >>> a, b = make_rat(3, 4), make_rat(5, 3)
    >>> c = mul_rat(a, b)
    >>> num(c)
    15
    >>> den(c)
    12
    """
"*** YOUR CODE HERE ***"
new_num = num(a) * num(b) new_den = den(a) * den(b) return make_rat(new_num, new_den)
def div_rat(a, b): """Divides two rational numbers A and B. Keep in mind that A / B is equivalent to A * (1 / B). For example, (3 / 4) / (5 / 3) = (9 / 20) >>> a, b = make_rat(3, 4), make_rat(5, 3) >>> c = div_rat(a, b) >>> num(c) 9 >>> den(c) 20 """
"*** YOUR CODE HERE ***"
new_num = num(a) * den(b) new_den = num(b) * den(a) return make_rat(new_num, new_den) # Alternate solution def div_rat(a, b): inverse = make_rat(den(b), num(b)) return mul_rat(a, inverse)

Use OK to test your code:

python ok -q mul_rat --local
python ok -q div_rat --local

Question 5

Implement a function eq_rat, which checks if two rational numbers are equal. This isn't as straightforward as it seems.

For example, (2 / 3) is equal to (6 / 9).

def eq_rat(a, b):
    """Returns True if two rational numbers A and B are equal. For
    example, (2 / 3) = (6 / 9), so eq_rat would return True.

    >>> a, b = make_rat(2, 3), make_rat(6, 9)
    >>> eq_rat(a, b)
    True
    >>> c, d = make_rat(1, 4), make_rat(1, 2)
    >>> eq_rat(c, d)
    False
    """
"*** YOUR CODE HERE ***"
return num(a) * den(b) == num(b) * den(a)

Use OK to test your code:

python ok -q eq_rat --local

Challenge Question

Questions in this section are not required for submission. However, if you do a challenge question, you can skip another question on the lab without losing points.

Question 6: Depth

Define a function depth that, given a tree, t, and a value, v, finds the depth at which v appears as a datum in the tree. Depth here refers to distance from the root, t. The node t itself is at depth 0; its children are at depth 1, etc. Assume that v appears at most once in the tree. Return None if it does not appear.

def depth(t, v):
    """Returns the depth of value v in tree t if v is contained in t.
    If v is not in t, return None.

    >>> test_tree = tree(1,
    ...                  (tree(2,
    ...                        (tree(3,
    ...                              (tree(4),
    ...                               tree(5))),
    ...                         tree(6,
    ...                              (tree(7),
    ...                               tree(8))))),
    ...                  (tree(9,
    ...                        (tree(10,
    ...                              (tree(11),
    ...                               tree(12))),
    ...                         tree(13,
    ...                              (tree(14),
    ...                               tree(15))))))))
    >>> depth(test_tree, 1)
    0
    >>> depth(test_tree, 42) # Returns None
    >>> depth(test_tree, 6)
    2
    >>> depth(test_tree, 15)
    3
    """
"*** YOUR CODE HERE ***"
if root(t) == v: return 0 elif branches(t): child_depths = map(lambda c: depth(c, v), branches(t)) vals = list(filter(lambda x: x is not None, child_depths)) if vals!=[]: return 1 + vals[0] return None

Use OK to test your code:

python ok -q depth --local