In these exercises you will implement a few algorithms to process a singly-linked list data structure defined to be generic for any type T. Just like in the List Utility functions, these functions will be pure and are well suited for writing test cases. As such, you will also write test cases demonstrating their usage and verifying their correctness.
In the comp110/exercises
directory, create a subdirectory named ex11_generics
.
generics.py
In the ex11_generics
directory, create a file named generics.py
and establish the following starting contents (which is a reproduction of what we wrote in class today):
"""Utility functions that operate on a generically defined linked list Node."""
from __future__ import annotations
from typing import TypeVar, Generic, Optional, List
T = TypeVar("T")
class Node(Generic[T]):
"""A generic linked list Node for any data type T."""
data: T
next: Optional[Node[T]]
def __init__(self, data: T, next: Optional[Node[T]]):
"""Construct a Node in a Linked list that forms head of a new list."""
self.data = data
self.next = next
def __repr__(self) -> str:
"""Produce a string representation of a linked list."""
if self.next is None:
return f"{self.data} -> None"
else:
return f"{self.data} -> {self.next}"
def is_equal(lhs: Optional[Node[T]], rhs: Optional[Node[T]]) -> bool:
"""Test if two linked lists are deeply (values and order) equal to one another."""
if lhs is None and rhs is None:
return True
elif lhs is None or rhs is None or lhs.data != rhs.data:
return False
else:
return is_equal(lhs.next, rhs.next)
Notice the is_equal
function is reproduced from what we developed together in lecture today. You will want to define this function in the same module as Node
to be sure they are referencing the same Node
definition.
generics_test.py
Establish a file for your test definitions. For today’s exercise we expect you to be able to setup the test file independently. Look toward the last exercise for inspiration and remember to check equality you’ll need to use the function we developed in lecture today (and reproduced in generics.py
as is_equal
).
You must use recursive function calls to implement the functions below. If you use loops your work for that function will be disqualified.
linkify
Given a List[T]
, your linkify
function should return a Linked List of Nodes with the same values, in the same order, as the input List
.
You will find it helpful to use Python’s slice subscription notation here, which we haven’t discussed in full but you should now be able to pickup quickly. Try the following in the REPL:
>>> items = [10, 20, 30, 40]
>>> items[1]
20
>>> items[1:3]
[20, 30]
>>> items[:3]
[10, 20, 30]
>>> items[1:]
[20, 30, 40]
Notice when using slice notation a new List
is returned to you whose values start with the first index number, inclusive, and end with the index number following the colon, exclusive. If you leave off the starting index, it defaults to 0
. If you leave off the ending index, it defaults to the len
of the List.
A skeleton for linkify
is:
def linkify(items: List[T]) -> Optional[Node[T]]:
return None
Example usage:
>>> linkify([1, 2, 3])
1 -> 2 -> 3 -> None
>>> linkify(["Hello", "World"])
Hello -> World -> None
After you are certain of the correctness of your linkify
function, you may find it valuable to use in writing test cases for the following functions.
scale
Given a head Node[float]
of a linked list and a float
factor
to scale by, return a new linked list of Node[float]
s where each value in the original list is scaled (multiplied) by the scaling factor
.
Example usage:
>>> scale(linkify([1.0, 2.0, 3.0]), 2.0)
2.0 -> 4.0 -> 6.0 -> None
Skeleton function implementation:
def scale(head: Optional[Node[float]], factor: float) -> Optional[Node[float]]:
return None
concat
Given two linked lists, return an entirely new linked list where the first list is followed by the second list. You should not reuse any existing Node objects in the implementation of this function.
Hint #1: You will need to fully process both lists.
Hint #2: You do not need to make forward progress in both lists at each step.
Caution: Your function may appear to work locally but will have points deducted if you do not make new Node objects that correspond to each of the original Node objects in both linked lists.
Example usage:
>>> scale(linkify([1.0, 2.0, 3.0]), 2.0)
2.0 -> 4.0 -> 6.0 -> None
>>> concat(linkify([1, 2, 3]), linkify([4, 5, 6]))
1 -> 2 -> 3 -> 4 -> 5 -> 6 -> None
>>> concat(linkify(["Hello"]), linkify(["World"]))
Hello -> World -> None
Skeleton function implementation:
def concat(lhs: Optional[Node[T]], rhs: Optional[Node[T]]) -> Optional[Node[T]]:
return None
You should write your own tests to be confident of your implementations. None of these functions require much code to complete, but will take some mental gymnastics to think about approaching recursively.
Remember: Focus on what you need to do with only the head node and then leave the rest of the problem working through the rest of the list to the recursive function call.
It is possible to write functionally correct programs that do not satisfy the constraints of the type checker. The reason for this a combination of naivete on the type checker’s part, as well as some gnarly theoretical computer science related to the halting problem which you’ll learn about in COMP455.
The short story is a type checker is typically not going to be able to reason through every possible code path in the same way you’re able to. For example, if you have a function that declares it returns Optional[int]
, and you call that function, then you are going to need to check for the possibility of None
as a return value. This is true even if before the function call you made some other test about the argument that convinced you the function call would return an int
. Typically this is a sign you can rewrite your logic to be both easier to follow while also satisfying the type checker. Think about making your test of None
based on the returned value of the recursive call rather than on the argument before the call is made. Play around with the code and challenge yourself to rewrite it. This is a puzzle and it’s good practice that forces you to grapple with solving the same problem from different angles of attack.