Abstraction Barrier
Learning Objectives
At the end of this sub-unit, students should
- appreciate the benefit of not breaking abstraction barrier.
Function Classification
When designing an ADT, there is a general categories of functions that are often created. In our burger example, we did not use all of these. As such, we will be using an example of rational number.
Rational Number
In mathematics, a rational number is a number that can be expressed as the quotient or fraction \({\displaystyle {\tfrac {p}{q}}}\) of two integers, a numerator \(p\) and a non-zero denominator \(q\).
Straight for the definition, we can see that we need to store two integers: \(p\) and \(q\). This gives us the following basic design.
Basic Rational Number Design
Using this design, we can classify the functions as follows.
Function Classification
Category | Description | Example |
---|---|---|
Constructor | Create the data | make_rat(numer, denom) |
Accessor | Retrieve the field from data | get_numer(rat) |
Mutator | Update the field in data | set_denom(rat, denom) |
Using this very basic design, we can solve anything involving our rational number.
In fact, mutators may not be necessary in this case because it is simply a wrapper for make_rat
in disguise.
More complicated mutators are possible so we keep the classification.
As before, we can ask ourselves why not just use tuple directly? There are some subtle problems with the design which we have not address.
- Is
(1, 2)
equivalent to(2, 4)
? - Can we construct
(1, 0)
?
If we have simply used tuple directly without calling make_rat
or other functions above, we need to change the design anywhere the tuple is used.
On the other hand, if we simply used the functions above, we only need to update this function and all usages will automatically use the new design.
Improved Rational Number Design
But what can we do with the rational number? Not much. We still need to write all the operations we want to do with the rational number. This does not belong to the classifications above, but a general one. So let us write arithmetic operations based on the definition below.
Operations on Rational Number
Once we have these functions, we may want to limit the capability by removing certain functions. The functions we kept are often called the application programming interface (API). In a more object-oriented language, we may even want to specify which functions are available to others and which are available internally. Unfortunately, in Python, these are not easy to do.
We will now consider only the following functions available for our rational number data.
Rational Number API
make_rat(numer, denom)
: construct \(\frac{\text{numer}}{\text{denom}}\).add_rat(r1, r2)
: \(r_1 + r_2\).sub_rat(r1, r2)
: \(r_1 - r_2\).mul_rat(r1, r2)
: \(r_1 \times r_2\).div_rat(r1, r2)
: \(r_1 \div r_2\).eq_rat(r1, r2)
: \(r_1 = r_2\).
Keeping Abstraction Barrier
Given the abstraction for the rational number above, we want to construct other data type that uses rational number. What we also want is to not break abstraction. To do that, we need to forget about the implementation details of rational number. Instead, we can only use the functions defined in the rational number API.
To make this discussion more concrete, we will construct another implementation. This construction is created using an inefficient implementation of constructor and accessors below. You do not have to understand them, you do not even need to know them. What is important is to know that you cannot assume which implementation we will be using.
Alternative Rational Number Design
What we want to do now is to create complex number.
Complex Number
In mathematics, a complex number is an element of a number system that extends the real numbers with a specific element denoted \(i\), called the imaginary unit and satisfying the equation \({\displaystyle i^{2}=-1}\); every complex number can be expressed in the form \({\displaystyle a+bi}\), where \(a\) and \(b\) are real numbers.
However, instead of \(a\) and \(b\) being real numbers, we will be letting them be rational numbers instead. The operations we need to implement are constructor, addition, subtraction, multiplication, and equality. Besides the construcor, the operations are defined below. We will need to decide on our data representation.
First, let us give a preliminary design for constructor and accessors. There is a trick we can do for constructor: simply put all parameters in a tuple. The reason is rather straightforward. If the entire input parameters is needed for computation, we have not lost any parameters as we are keeping them. If some parameters are not necessary, we can simply ignore them during computation.
So keeping all the parameters is the most straightforward way to define a constructor. From there, the accessor is rather trivial. We will also introduce a notation to help us which is called a type annotation. This type annotation will be ignored by Python as it is a comment and it is intended for other programmers. We annotate a function with the following type annotation.
The annotation after ->
is the return type.
These types are not Python type, but our abstract type.
Basic Complex Number Design
Consider the following badly designed code for addition. This code breaks abstraction. At Line 6 and 7, we are assuming that our rational number abstraction is a tuple. But it may not be, that is the reason why we have abstraction.
Complex Addition
If we run the above code with our tuple implementation of rational number, we get the correct output. Unfortunately, if we use the alternative design, it will not work. Of course, we can always force our way to make it work with a lot of implementations by detecting what the implementation is. But that is kind of a waste of time.
Instead, we should not go against the design and use the available API. We should think of it like a puzzle where we are given a limited capability and we can only solve within those capabilities. So instead of performing the addition of real numbers on our own, we need to invoke the function that performs addition of rational number. One possible good design is shown below.
Operations on Complex Number
With this implementation, we can actually use either implementation for rational number.
Too see why, let us look at add_complex
and consider all the functions used.
We will annotate the functions with the type annotation we did before.
Function | Type Annotation |
---|---|
get_real(c) | (c::complex) -> rational |
get_imag(c) | (c::complex) -> rational |
add_rat(r1, r2) | (r1::rational, r2::rational) -> rational |
make_complex(a, b) | (r1::rational, r2::rational) -> complex |
Now, we can check the variables and figure out the type based on these type annotations.
c1::complex
from the assumption in the type annotation.c2::complex
from the assumption in the type annotation.a1::rational
because it comes from the result ofget_real(c1)
andc1::complex
.b1::rational
because it comes from the result ofget_imag(c1)
andc1::complex
.a2::rational
because it comes from the result ofget_real(c2)
andc2::complex
.b2::rational
because it comes from the result ofget_imag(c2)
andc2::complex
.
Finally, we consider the final return expression.
make_complex(add_rat(a1, a2), add_rat(b1, b2))
- Result of
add_rat(a1, a2)
isrational
becausea1::rational
,a2::rational
, andadd_rat
returnsrational
when given tworational
parameters. - Result of
add_rat(b1, b2)
isrational
becauseb1::rational
,b2::rational
, andadd_rat
returnsrational
when given tworational
parameters. - Result of
make_complex(..., ...)
iscomplex
because lhs isrational
, rhs isrational
, andmake_complex
returnscomplex
when given tworationals
.
- Result of
Notice that in our arguments above, we have never mentioned that we are using tuple
or str
.
The only data types used are the abstract data type of rational
and complex
.
Using this abstraction, we do not care how rational
and complex
are implemented.
This is the kind of argument we need to keep on reminding ourselves in order to avoid breaking abstraction. Unfortunately, the Python programming language provides no support for this. We say that Python is dynamically typed due to this. Other languages that are statically typed will perform the check automatically for us.
Due to the dynamic nature of Python, we introduce our own notation to indicate types as comment. The programmer will be the one checking manually for correctness and these comments are intended simply as a reminder. In fact, it is better to write them as documentation string as follows.