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.