Procedural, functional and data abstraction
Information hiding, hiding the values, the method and the representation, and swapping a data type's insides.
Do this lesson in the simulatorThe last lesson abstracted away details of the world. This one abstracts away details of the program: the particular values in a calculation, the method used to get an answer, and the way data is stored. Each kind has an exam name, and together they explain why large programs can be built by many people who never read each other's code.
Information hiding
Information hiding is hiding all the details of an object that do not contribute to its essential characteristics. Whoever uses a part of a program should need to know what it does and how to ask for it (its interface), never how it works inside (its implementation).
Hiding the details has three benefits:
- the user of a part has less to understand, so it is easier to use correctly;
- the inside can be changed or fixed without breaking any code that uses it, as long as the interface stays the same;
- nobody can come to depend on a detail by accident, such as reaching into a list that was meant to be private.
The robot library is an example. distance() hands back a number in cm. Whether that number came from a laser time-of-flight chip, an averaging filter or a simulator is hidden, which is why the same program runs on the real robot and in the browser.
Procedural abstraction
Start with one particular calculation: the area of the robot's 30 cm by 20 cm test pad.
print(30 * 20)
If you abstract away the actual values, what is left is a computational pattern: multiply a width by a height. Giving that pattern a name and parameters makes it a procedure. Procedural abstraction is this step: the result is a method that works for any values.
def area(width, height):
return width * height
print(area(30, 20))
print(area(100, 100))
Functional abstraction
Procedural abstraction still exposes the method: anyone reading area sees it multiplies. Functional abstraction goes one step further and disregards the particular computation method as well. All that matters is the relationship between input and output. The caller of a function knows that it maps these inputs to that output, and nothing about how.
Here are two functions that give the nearest reading in a list. Their methods are completely different, but as functional abstractions they are the same function:
def nearest_by_scan(readings):
best = readings[0]
for cm in readings:
if cm < best:
best = cm
return best
def nearest_by_sorting(readings):
ordered = sorted(readings)
return ordered[0]
readings = [48.5, 27.0, 63.2, 31.9]
print(nearest_by_scan(readings), nearest_by_sorting(readings))
A caller who only knows "nearest(readings) gives the smallest reading" can use either, and would never notice if one were swapped for the other. Python's own min and sorted are used this way every day by people who have no idea which algorithms are inside.
Data abstraction
Data abstraction hides how data is actually represented. The program works with a new kind of data object through a set of operations, and the representation underneath can be anything that makes those operations work. New kinds of data object are built from ones that already exist.
A lap timer is a good small example. The interface is three operations: make a time, add seconds to it, and show it. Here are two representations:
# representation 1: a list [minutes, seconds]
def new_time(minutes, seconds):
return [minutes, seconds]
def add_seconds(t, s):
total = t[0] * 60 + t[1] + s
return [total // 60, total % 60]
def show(t):
return f"{t[0]}:{t[1]:02d}"
lap = new_time(1, 50)
lap = add_seconds(lap, 25)
print(show(lap))
# representation 2: one whole number of seconds
def new_time(minutes, seconds):
return minutes * 60 + seconds
def add_seconds(t, s):
return t + s
def show(t):
return f"{t // 60}:{t % 60:02d}"
lap = new_time(1, 50)
lap = add_seconds(lap, 25)
print(show(lap))
The last four lines of each cell are identical and print the same 2:15. Code that sticks to the operations cannot tell which representation it has. That is data abstraction, and it is exactly how an abstract data type such as a stack is made: you can build a stack from an array and a pointer to its top, or from a linked list, and the code that pushes and pops never knows which (module A3).
| Kind | What is hidden | The user still knows |
|---|---|---|
| Procedural abstraction | the particular values | the method, applied to any values |
| Functional abstraction | the method as well | only what output each input gives |
| Data abstraction | how the data is represented | the operations on it |
| Information hiding | every detail not essential to using it | the interface |
Task: swap the insides
The robot stands in the middle of the mat, turns through eight 45 degree steps and logs a distance at each. The starter's log is a list, and the program at the bottom uses it only through five operations: new_log(), add_reading(log, cm), count(log), mean(log) and nearest(log).
Change the representation without changing the program at the bottom:
- a log must now be a dictionary holding only three figures: how many readings there have been, their running total, and the smallest reading so far;
new_log()returns an empty log;add_reading(log, cm)updates the figures for a readingcm(a number of cm) and returns nothing;count(log)returns the number of readings;mean(log)returns the mean reading;nearest(log)returns the smallest reading;- do not keep the readings, and do not use
append,lenorsumanywhere.
The program at the bottom must still print count 8, then mean <m> and nearest <n> with exactly the values the list version prints.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def new_log():
return []
def add_reading(log, cm):
log.append(cm)
def count(log):
return len(log)
def mean(log):
return sum(log) / len(log)
def nearest(log):
return min(log)
# the program that uses the log: do not change anything below this line
log = new_log()
for i in range(8):
add_reading(log, distance())
turn_right(40, angle=45)
print("count", count(log))
print("mean", round(mean(log), 1))
print("nearest", nearest(log))
Challenges
- Add a sixth operation,
farthest(log). What must change in the representation, and what must not change in the program at the bottom? - The dictionary version cannot give the median reading. Explain why, and what that says about choosing a representation.
- Is
sorteda procedural or a functional abstraction for someone who calls it? Justify your answer.