0:01
In this session we are going to stay a bit with the rational
numbers we introduced in the last session and are going to explore them further.
We are going to introduce in particular an important cornerstone of software engineering,
namely data abstraction, and show how it relates to the model of classes that we've introduced earlier.
So, in this session we will learn several new aspects about classes and objects.
Let's start with the worksheet that we had at the end of last session.
I will just change this, make a new example here.
We'll say, y dot add of y.
What does that give? Well, it gives us 70 over 49.
What you've seen here is that, that's a number that's not as simple as possible.
I would have expected a simpler number, a number maybe ten over seven.
So, why is that? Well, it turns out to be able to do that,
we need to still simplify the rational number.
When we produce them with the addition and multiplication operators, it could be that
we end up with numerators and denominators that can be further simplified by dividing
both with a common divisor. And that operation needs to be done so that we can
print rational numbers in the simplest possible form.
We could, of course, implement this in each rational operation. Add a
simplification step to add and multiply and subtract and so on. But, it would be
very easy to forget this division in an operation. Also, it would violate the DRY
principal, don't repeat yourself. So, a better alternative would be to perform the
simplification just once, and the natural place for that would be in the
initialization code of the class rational, that's when we create the rational object.
Alright. So, let's see how we would do that in the worksheet.
What I'll do first is I will retake the definition of gcd that we have seen the
previous week, and then make it a method of class rational.
What's important is that I put the modifier private in front of it because I
do not want that clients of class rational can see gcd. It's strictly for
implementation purposes here. The next thing I do is I define a
private value, g, which is the greatest common divisor of x and y.
2:28
And then, when I create a numerator, I'll say the numerator is x divided by g, and
the denominator is y divided by g. Let's see whether anything changes.
Well, my addition operation now yields the rational in simplified form and that's
what we wanted. So, note that, gcd and g are private
members of class rational. We can only access them from inside the
rational class. In this example, we've calculated the gcd
immediately here on initialization of the class, so that its value can be reused in
the calculations of numer and denom here so we don't have to recalculate gcd every
time someone calls numer and denom. We could also change that, of course.
We could call GCD in the code of numer and denom like that.
So that way we avoid the additional seal G, and it could be advantages if the
functions numer and denom are not called very often.
Then we could amortize the additional cost of the GCD operations here.
What we could equally well do is turn numer and denom into vowels, so that they
are computed only once. So now that would be advantageous if the
functions numer and denom are called very often.
Because, in that case, we've already computed what they are, and we do not
repeat the computations. What's important here is that, no matter
which of the three alternatives we choose clients observe exactly the same behavior
in each case. So, this ability to choose different
implementations of the data without affecting clients is called data
abstraction. And data abstraction is one of the
cornerstones of software engineering. It's a very important principle, in
particular, as systems grow large. In the next step, we want to add two more
methods to our class rational. One method less, which compares two
rational values, and the other, maximum, which takes the maximum of two values.
Let's start with less. So, we would take a rational.
When is this rational that you see here less than the other?
Well, it would be if the numerator times the denominator of the other function is
less than the numerator of the other rational times our own denominator.
We've simply multiplied both sides with the both denominators, and that's what we
arrive at. So, let's test it.
5:26
So, what could that be? The maximum of the current rational and
the parameter. Well, we'd really be able, want to be able
to use less here. And we want to say, well, if the current
rational number is less than the other rational number then, the other rational
number, otherwise the current one. But, that means we have to refer to our
rational number as a whole. And, in fact, there is a way to do that in
all, most object oriented languages this is either called "this" or "self".
So, this refers to the current rational. So it would, would say if this is less
than that, then we'll return that, otherwise, we return this.
And we can also test it. X maximum of y would be five, seven,
because that's the bigger of the two. So, we've seen that, on the inside of a
class, the name this represents the object on which the current method is executed.
And you've seen that this is essential for certain operations such as Maximum where
we have to return the whole rational number as a result, as you see here.
Okay. So, now that we are there, We can actually make a further
simplification. If we refer to a name x in a class,
that's really just an abbreviation for this dot x. So, the, the members of a
class can always be referenced with this as the prefix.
So, an equivalent way to formulate the less method is as follows.
That we say this.numer times that.denom, less than that.numer times this.denom.
And together, with the choice of our parameter name, now you see why we've
called it that. That gives us a nice symmetry in the
operations between the left operand and the right operand.
Okay. As a next step, let's look at some of the
restrictions we have to impose on rationals.
As a motivation, let me create a rational val strange equals new rational one over
zero. And then, add strange to itself. What do we get?
We get an arithmetic exception division by zero.
Because of course, a rational that has a denominator of zero doesn't exist.
It's not a rational number. So, how can we guard against users
creating illegal rationals like that? One thing we could do is add a requirement
into our class. So, show you how that's done.
We could require that y is different from zero, and then we could say denominator
must be non zero. If we do that and look at the worksheet,
then now our exception has changed. It now says, illegal argument exception,
Denominator must be nonzero. So, a requirement is a test that is
performed when the class is initialized here, and if that test fails, then you,
you will get an exception, in this case, an illegal argument exception.
So, let me remove the problematic lines to get a clean work sheet again.
The require function that we've called in class rational is actually a predefined
function. So, it's already defined for us.
And it takes a condition, that's the test and an optional message string.
In our case, that was the denominator must be positive.
If the condition here is false, then require will throw any illegal argument
exception, And that exception will contain the
message string. Designs require that's also another test
which is called assert. Assert takes a condition like require and
also an optional message string so you could use it like this for instance,
X equals square root of y. And then, you assert that x must be greater or equal
to zero. Like require, a failing assert will also
throw an exception, but it's a different one.
Now, it will throw an assertion error instead of, before, an illegal argument exception.
That, in fact, reflects a difference in intent.
Require is used to enforce a precondition on the caller of a function or the creator
of an object of some class. Whereas, assert is used to check the code of the
function itself. So, if a precondition fails, then you get
an illegal argument exception. Whereas, if an assertion fails and it's not the
caller's fault and consequently you get an assertion error.
Another syntactic construct we're going to cover is constructors.
In fact, in Scala, every class already implicitly introduces a constructor which
is called the primary constructor of the class.
That primary constructor simply takes the parameters of the class and executes all
statements in the class body. So, for instance, the constructor of class
rational would take the x and y as the parameter, and then execute the class
body. So, that means, it would execute the require,
It would execute the value definition here, and for the def, there's nothing to
execute. If you know Java, then you're used to
classes having several constructors. In fact, in Scala, that's also possible,
even though the syntax is different from Java.
So, let's say we want to have a second constructor for class Rational that only
takes one integer: denominator. In that case, we would assume that the
nominator could be zero. We can just write as follows. We can write
def this x int, And then we write x and one. So, what you
see here is a second usage of the keyword, this, now used in function position.
If this is used as a function, then it means a constructor of the class. So here,
we define a second constructor for class rational in addition to the primary one.
It only takes a single argument, and what it does it calls another constructor with
the two arguments. That constructor takes two arguments is in
fact the implicit primary constructor of class rational.
So, if we do that, then we can use class rational in a simpler way.
We could, for instance, say new rational of two,
13:10
Okay. So, let's see how we would solve this example.
So, I can leave the gcd function because I will still need it, but I remove the
definition of the value G as well as the two divisions here, so that means that
rationals are, from now on, kept unsimplified.
You see that our x add, sorry, our y example jumped back to seventy over
forty-nine instead of ten over seven. So, what we do instead is we go into the
toString function and do something there.
What I propose is that we define our gcd function in toString. val g equals gcd
of numer and denom. And then, we divide numer by d and denom
by g, and that would do the trick. So, now we keep the rational number unsimplified.
But, before we print it, we perform the simplification. And in our case, all the
results really gave back the same value. So, is that always the case?
Well, the answer, actually, is no. It's only the case if the numerator or
denominator is small. The reason for that is that we are dealing
here with integers as the fields of a class rational.
And, so we might exceed the maximal number for an integer which is a bit more than
two billion. For that reason, it actually, it's
actually better to always normalize numbers as early as possible because that
means that we can perform more computations without running into
arithmetic overflows.