ICS 32A Fall 2023
Exercise Set 4

Due date and time: Monday, November 6, 11:59pm


Getting started

First of all, be sure that you've read the Reinforcement Exercises page, which explains everything you'll need to know, generally, about how the reinforcement exercises will work this quarter. Make sure you read that page before you continue with this one.


Problem 1 (3 points)

Background

If you've ever used a web browser before, there's a good chance that you've seen a Uniform Resource Locator (URL) before. URLs provide a way to identify resources on the web — as well as other places online, as they're actually a more generic concept than you might at first think. For example, the URL for this page is below.

Because I've rendered the URL on this page as a link you can click, you'll be able to click that link and wind up right back where you started: on this page. But the URL is really just the text. One way you could store that into a variable in Python would like this.


url = 'https://www.ics.uci.edu/~thornton/ics32a/Exercises/Set4/index.html'

However, not every string is a valid URL; only some strings are. URLs, as it turns out, are a much more complex topic than they might appear at first blush. The original specification defining URLs, from 1994 (when I was about a year ahead of where you are in your studies now), weighs in at about 25 pages, which is the original definition of what is and isn't a valid URL, as well as what various URLs are intended to mean. We'll talk a lot more in detail about URLs and the web in the not-too-distant future in this course, but, for now, the basic idea suffices: A URL tells you where something is on the web, and there are rules governing whether a URL is valid and what it means.

The problem

Let's start with a simplification of the URL, which we'll call an SRL (Simplified Resource Locator). (There's no such thing as an SRL; we're making this up as we go along.) Suppose that these are the rules governing SRLs:

If we wanted to write a program in Python that used or manipulated SRLs, we'd have two somewhat related problems to solve:

  1. Use our understanding of the structure of SRLs to ensure that we never have any that are invalid.
  2. Given a valid SRL, use our understanding of what SRLs mean to be able to access its various component parts.

We could use strings to store SRLs, but strings don't solve either of the most important problems we need to solve. As a result, we'd have to spread that knowledge throughout our program: Any time we built one, we'd have to be careful that it be valid, while any time we used one, we'd have to slice it up to understand what it meant.

What would be better is a Python class that solves these problems. If we had a special type that knew about all of these details, we could have enforcement of the rules around SRLs, as well as one-stop-shopping for understanding what's inside of them, similar to what the pathlib.Path type did for us in Project 1.

Write a class called Locator. A Locator object represents an SRL, and provides the following methods.

Along with your class, you are required to write assert-based tests that exercise its functionality.

Safely testing with variables

As we've seen, assert-based tests are best written at the "top level" of a module, which is to say that they appear outside of any functions, interspersed with the code they test. For "pure" functions, which are those whose outputs are determined solely by their inputs, each assert statement can be self-contained, because we won't need any variables to store values; we just write expressions that are evaluated in place, checked by assert, and then, if they succeed, can safely be thrown away.


def duplicate(s: str) -> str:
    return s + s

assert duplicate('Boo') == 'BooBoo'
assert duplicate('z') == 'zz'
assert duplicate('') == ''

But what do we do when we want to test classes like the one we've asked you to build in this problem? In this case, we need to be able to create an object, store it in a variable, then call methods on it and see what their results are; it's not one function call we're testing, but sequences of behavior that we're testing. Before too long, we'll start using testing tools that allow us to do this kind of thing more cleanly, but all is not lost, even if all we're using is assert; we just need a way to isolate our tests, so that any variables we use aren't global.

The way we'll solve that problem is to write test functions. The test functions will contain assert statements, but then we'll make sure there are calls to these functions at the top level — ideally, right after each test function, so we have an obvious place to look for them. This way, any variable we create will be scoped to a function, which means it'll only last long enough to run our tests; that's a lot better than letting it be global to our module, where it will be visible to any code that uses our module.


# Imagine this is the Counter class from lecture
class Counter:
    ...


def _test_counters_count_upward() -> None:
    c1 = Counter()
    assert c1.count() == 1
    assert c1.count() == 2
    assert c1.count() == 3

_test_counters_count_upward()

def _test_peek_leaves_counters_unchanged() -> None:
    c1 = Counter()
    c1.count()
    c1.count()
    assert c1.peek() == 2
    assert c1.peek() == 2

_test_peek_leaves_counters_unchanged()

# I'd write more tests than this, but this demonstrates the technique

If you're thinking that this is a bit of a clunky process to follow — having to write functions and then remember to call each one — you're right! That's why we'll soon learn about some tools built into Python's standard library that automate some of the things we're doing by hand here. But, as we've seen all quarter, you can do a lot with meager skills; even where we are now, we have a pretty reasonable way to isolate the effect of our testing from everything else, so we should use what we know, then allow our gradually improving knowledge to make things better, a little bit at a time. (I'm still doing that, after all these years; I still learn new techniques that improve my work, and I expect I will for as long as I do this kind of work.)

What to submit

Submit one Python script named problem1.py, which contains the implementation of your Locator class, along with your assert-based tests of that class.

Docstrings are not required, since we've already fully agreed on what problem we're solving here.


Problem 2 (1 point)

One of the things we established during our discussion about Classes is the need to be strategic in our design. When we write a class, we're designing a new type, and the choices we make will influence whether objects of that type will be easy to use, or whether it will be too easy to make mistakes with them that lead to program misbehavior or crashes. While there are many things that Python will allow us to do wherever we want, there's value in isolating those things in the "right place." We've already learned a few design recipes (i.e., basic rules we can follow so that our designs will be simpler, easier to grow into much larger programs, and so on). As we learn the design recipes in any programming language, it's worth considering the forces that shaped those recipes; in other words, we should want to know why those recipes say what they say. One way to understand why is to imagine that they said something different, then consider what the impact would be.

A design recipe we saw when we talked about Classes was the idea that we should initialize all of an object's attributes in the __init__ method of that object's class. In a sentence or two, explain why we made that decision by assuming that we made the opposite decision — attributes can be initialized anywhere within the class instead — and describing how that could lead to trouble.

What to submit

Submit one PDF file named problem2.pdf, which contains your answer to this question.


Problem 3 (4 points)

Suppose that we had an interface shape, which represents the concept of a geometric shape in two-dimensional space, such as a circle or a rectangle. Of course, there are substantial differences between the different shapes that we might want to represent in a system like this, but there are also substantial similarities. In particular, there are things that all shapes ought to be able to do, even if the way they do those things might be quite different in each case. If we design an interface for those similarities (i.e., if the way we ask different kinds of shapes to do these jobs is always the same, even if there differences in how the jobs are done), then we should be able to use shapes for many purposes interchangeably.

Let's agree that all shapes have the following common behavior:

Your task here is do three things:

  1. Design a Python interface that allows all of the common behavior above. All I mean by "design a Python interface" is to decide what the names of the methods would be, what parameters you'd need to pass to those methods, and so on. One important thing to realize, though, is that your design has to be one that allows all kinds of shapes to solve each problem, e.g., the parameters for a method in your interface have to be the same for all kinds of shapes. (This echoes a concept we talked a lot about in the Duck Typing and Interfaces lecture.)
  2. Choose any three of the following shapes and implement a class for each. All of them should implement the interface you designed, and include whatever else they would need (e.g., attributes, additional public or private methods) that you think are necessary to be able to provide that interface.
    • Circle
    • Right triangle, oriented so that each of the two sides other than the hypotenuse run parallel to the x and y axes, respectively, and so that the hypotenuse runs from the top-rightmost point to the bottom-leftmost point within the shape.
    • Rectangle, oriented so that it has two sides running parallel to the x axis and the other two sides running parallel to the y axis.
    • Regular pentagon, oriented so that its bottommost side runs parallel to the x axis.
    • Regular hexagon, oriented so that two of its sides run parallel to the x axis but none of its sides run parallel to the y axis.
  3. Write a function that, given a sequence of shapes of any kind — including any that you've not yet implemented, but that you might later write that share the same interface — calculates the sum of their areas.

We're leaving you some design flexibility here (purposefully), so you'll be permitted to choose your own names for classes, methods, and so on. Use that flexibility wisely; that's not to say that we're fishing for whether you'll choose a particular name for something, but we're at least requiring that you choose a name that accurately reflects something's purpose.

Limitations

Your prior background in other programming languages (e.g., Java) might lead you to want to use a design technique called inheritance in this problem. While it's true that Python supports inheritance, its meaning isn't quite what you would expect if you've seen it in other programming languages, and we've not had a chance to discuss it yet, so inheritance is not permitted in your solution.

What to submit

Submit one Python module named problem3.py, which contains comments at the top that describe your interface, followed by your three classes, your function that calculates the sum of the areas of a shape, and assert-based tests for your classes and area-summing function. If you need variables in your assert-based tests, use the technique described in Problem 1 for isolating your tests from your module's global scope.

Docstrings are not required, but the names of your functions, methods, parameters, and attributes need to be well-chosen to reflect their purpose. If we can't tell that you've solved the problem we've assigned, we won't be able to give you credit for it.


Problem 4 (2 points)

When we discussed Duck Typing and Interfaces, one of the central themes was that of an interface. An interface is a concept whose power belies its simplicity; while interfaces are specified differently in Python than they are in many other programming languages, the idea is one that's common in programming. One way or another, all an interface really does is specify a set of characteristics a type needs to have in order to be compatible with some function, some class, or whatever.

In each of the following functions, there are (parts of) type annotations that have been specified opaquely as '???'. In each case, the variable or parameter they annotate have also been given intentionally non-descriptive names. For each function, propose a name for an interface (i.e., what you might replace '???' with) that describes each of the types that have been left unspecified, and specify what characteristics would be necessary to make a type compatible with that interface.

  1. 
    def render_scene(display: Display, x: list['???']) -> None:
        for obj in x:
            obj.draw(display)
    
  2. 
    def aggregate_length(x: list['???']) -> None:
        total = 0
    
        for obj in x:
            total += len(obj)
    
        return total
    
  3. 
    def find_largest_component(x: '???') -> None:
        max_size: int = 0
    
        y: '???' = x.all_components()
    
        while True:
            try:
                z: '???' = y.next_component()
    
                if z.size() > max_size:
                    max_size = z.size()
            except NoMoreComponentsError:
                break
    
        return max_size
    

What to submit

Submit one PDF file named problem4.pdf, which contains your answers to these questions.


Deliverables

In Canvas, you'll find a separate submission area for each problem. Submit your solution to each problem into the appropriate submission area. Be sure that you're submitting the correct file into the correct area (i.e., submitting your Problem 1 solution to the area for Problem 1, and so on). Under no circumstances will we offer credit for files submitted in the incorrect area.

Submit each file as-is, without putting it into a Zip file or arranging it in any other way. If we asked for a PDF, for example, all we want is a PDF; no more, no less. If you submit something other than what we asked for (e.g., a text file when we asked for a PDF, even if its filename ends in .pdf), we will not be offering you any credit on the submission. There are no exceptions to this rule.

Of course, you should also be aware that you're responsible for submitting precisely the version of your work that you want graded. We won't regrade an exercise simply because you submitted the wrong version accidentally.

Can I submit after the deadline?

Unlike some of the projects in this course, the reinforcement exercises cannot be submitted after the deadline; there is no late policy for these. Each is worth only 3% of your grade, with the lowest score dropped — see the Reinforcement Exercises page for details — so it's not a disaster if you miss one of them along the way.

You're responsible for making a submission in order to receive credit, which means you'll want to be sure that you've remembered to submit your work and verify in Canvas that it's been received. A later claim of having forgotten to submit your work or having misremembered the due date will not be grounds for a resubmission under any circumstances.

What do I do if Canvas adjusts my filename?

Canvas will sometimes modify your filenames when you submit them (e.g., by adding a numbering scheme like -1 or a long sequence of hexadecimal digits to its name). In general, this is fine; as long as the file you submitted has the correct name prior to submission, we'll be able to obtain it with that same name, even if Canvas adjusts it.