So in this unit we are going to learn about data abstraction. So the previous example has shown that rational numbers aren't always represented in the simplest form. For instance, we saw a rational number 66 to over 42. So you might have expected to see 33 over 21 instead, which is the same rational number, but reduced the numerator and denominator opposed the smallest possible. So what we would like to is simplify rational numbers, which means reduce them to the smallest numerator and denominator by dividing both with a divisor. We could of course implement this in each rational operation, but it would be easy to forget this division in an operation and besides, we don't want to repeat ourselves over and over again for each of the implemented operations. So better alternative is to simplify the representation of the class when the objects are constructed. So the way we could do that is we can define a method GCD. That's the greatest common divisor function that you've already seen in class rational. Now GCD is used only internally, so it's labeled here private clients of class rational can't call the GCD directly. It's used only for the internal functioning of class rational. Then we have another value G, which is the GCD of the numerator and the denominator which is always also private. You shouldn't access it from the outside. And then finally we have the numerator and the denominator is X divided by G and Y divided by G. So that gives us the smallest possible numerator and denominator. So in that example we have calculated the GCD immediately so that its value can be re used in the calculations of numer and denom. It's also possible to call GCD in the code of numer and denom directly. So here instead of having a separate variable G we would just divide by GCD here and here. And that can be advantages if we wouldn't expect the functions numer and denom to be called very often, it's equally possible to turn numer and denom into vals so they are computed only ones. And that can be advantages if the functions numer and denoms are called often which we would expect this in normal case. So the normal case would be a private method GCD which is called in the initializer for the numer and denom values. So the point is that if you're using rationals, clients of class rationals observe exactly the same behavior in each case it doesn't matter where exactly you define the GCD. Whether you create a separate value to hold it or whether numer and denam are values or methods. This ability to choose different implementations of the data without affecting clients is called data abstraction. And it's a cornerstone of software engineering really, because it allows you to adapt and refine the implementation without affecting the clients. You can see locally in a class. You can reason locally about a class without having to keep the whole program in your head and track the side effects that your class might have on all the rest of the program. So another important concept when we talk about objects and classes is the self reference. Inside a class, the name this represents the object on which the current method is executed. So for instance, if we would like to add functions less and max to the class rational, then here's what we would do be defined in less method as usual. So, if you want to say X1 over Y1 is less X2 over Y2. And that's equivalent to essentially just multiply everything by one and y2. So that's equivalent to X1, Y2 less than X2 by one. And that's what we implemented in this formula here. Now, let's look at the max method to implement max, we would ask whether the current rational number is less than the right hand side argument. In that case we return the right hand side arguments and otherwise we return the rational number itself. So you can see here that the ability to name the current object with this is essential because otherwise we couldn't return the current object as a result of an operation. The other occurrence of this was here where we used the less method on the current object, that one is equivalent to just riding less of that. So, in general, a simple name that refers to another member of the class is just an abbreviation for this diagram. That's an equivalent way to formulate less is as follows this.numer times that.denom less than that.numer times this.denom. So now we have made it explicit everywhere that we mean the current object, but we can also leave out these two references to this and just write numer and denom as before. The meaning is exactly the same. So let's look at more tools that are useful for dealing with data abstractions and classes, let's say in our class rational, we want to require that the denominator is positive. We can enforce that by calling the require function so we could start class rational with a function which is require y greater zero. Denominator must be positive. Require is a predefined function, it takes a condition and optionally a message string. If the condition is false, then it throws an exception called an illegal argument exception and that takes the message string as an argument, throwing an exception will usually terminate the program with the message string. But there are ways to handle it later and we learn about those later on. For now we can think of throwing an exception as just the way to terminate the program. Similar to require we also have the predefined assert function, assert like require takes a condition in an optional message string as parameters. So for instance, we could write val x equals square root y, assert x greater or equal zero. An assertion like this state something that should be true. If the condition is false, then it will also throws an exception, but it's a different one. It's called an assertion error for assert whereas it was an illegal argument exception for require that reflects a difference in intent, requires used to enforce a precondition on the caller of a function. So if require goes wrong, then the 40s with the code that called the function. Whereas assert is used to check the code of the function itself. So if assert is wrong, then essentially we have a bug in the implementation of the function. It was called probably the correct arguments but the implementation is wrong. So we've seen that a class implicitly introduces not just the type, but also a constructor away to construct elements of that class. This implicit constructor is also called the primary constructor of a class. So the primary constructor takes the parameters of the class and executes all statements in the class body such as the require a couple of slides back or the value definitions of numerator and denominator or any other statement that's in the body of a class. So when as a primary constructor, that implies that there could also be other constructors. And in fact, that's the case. Scala allows the declaration of auxiliary constructors which are just methods named this. So here's an example. Let's say you want to have a constructor for class rational, that takes a single argument. So we want to be able to write something like rational of two and that should just give the number two over one. So the integer number two expressed as a rational. So we can do this by defining an auxiliary method this that takes the single integer, the numerator and that creates a rational with the numerator and one as the denominator. Now I've put everything we've covered so far in the worksheet with the definition of the class rational. So we have there required the secondary constructor the this than the normalization with GCD than all the methods and so on. So in the examples, we see that something doesn't work. So what do we see? Well, It said denominator must be positive, but we got eight divided by -21. So the denominator was negative. Where did we go wrong? So that we can debug it a little bit and comment of the thing. So it was the first subtraction. That is already the case, right? That went already wrong. So if we think about it, where could the problem be? In fact, it turns out the problem is in the definition of GCD. And the calls to GCD here because the definition of GCD assumes that the two operations are both positive. Whereas here the denominator, the y must be positive, but the x can be negative. So the GCD with a negative number, the way we've defined it would come out as negative again. And to fix that we would have to define the absolute value of the x value. So that's in fact the greatest common divisor through which we have to divide. We don't there's no need to take the absolute number value from the y because they require here already indicates that why must be positive. So with that change we have that the final expression evaluates correctly without throwing an exception. Another thing you saw is that the class is getting quite long so it's sometimes hard to see where it ends and to help here there's a way to specify that. So in scala you can write a so called end marker which tells you what's the end of the class. If you get the column wrong like this then you would get an error which says that the end marker is misaligned, so the end marker has to start at exactly the same column as the class that it closes. So generally an end marker is followed by the name that's defined in the definition that ends at this point and it must align with the opening key word in this case, the end must be in the same column as the class. And markets can also be used for other constructs. For instance that can be used to terminate a death or a well like what you see here or to terminate a control construct like an if then else that spends several lines. If it's a control construct that's terminated, you follow the end by the name of the control construct instead of the name that's being defined. Because of course for control constructs, there is no name that is defined. So here's an exercise for you, modify the rational class so that rational numbers are kept unsimplified. So no GCD, but the simplification is applied when numbers are converted to strengths. Two clients observed the same behavior when interacting with the rational class. Yes, no or the 3rd choice is yes. For small sizes of denominators, enumerators and small number of operations. So let's see, we would you leave the simplifications of the numerator and denominator there now in simplified and we would add the simplifications when they are printed. That's all we have to do, now in this example we see the same output. So the question is is that always the case? Let's think a bit about it. And I believe the fair answer is really three. For small sizes of denominators and numerator is you wouldn't see a change and as long as the number of operations is small, why would you see a change if denominators enumerators can get very large because of the problem of overflow? If the rational numbers are not simplified, the denominators enumerators can get increasingly larger until they don't fit into the range of an integer anymore.