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. 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. Is x dot less y? And the answer is true, as expected. The next function we want to do is the maximum function. 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, And that then would give the rational two over one. So, we could conveniently omit the denominator if it's one. So, let's do an exercise. Modify the rational class so that all rational numbers are kept unsimplified internally, But the simplification is applied when numbers are converted to strings. When you've done that, think about where the clients of the new class rational would observe the same behavior when interacting with it, same behavior as the previous one. Possible answers are yes, no, or yes for small sizes of denominators and nominators and small numbers of operations. 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.