Over the last three sessions, you've learned about objects and matters. But so far these are all instances of a single class. This is going to change in this session. We're going to introduce class hierarchies of classes that extend each other. An important aspect once we have class hierarchies is the model of evaluation. Because then the actual method called might depend on the runtime type of the receiver of that method, this concept is called dynamic binding. It's one of the standard concepts in objectives of programming, and you're going to find out how our model of family writing actually already naturally supports this concept. So dynamic binding and family writing go very naturally together. In the last session we have looked in-depth at a lot of different aspects of a single class. What we're gonna do in this session is generalize that so that we now look at class hierarchies where more than one class cooperates to achieve a certain task. So as a running example in this session, we consider the task of writing a set of classes that implements sets of integers. And these integers that will be called IntSet and support two operations to include an element x in the IntSet and contains, which is a test whether an element x is a member of the set. So to do that, I will open a fresh worksheet. Call it intsets and. Add the definition of the class here. What you see what's particular is that we've given you two definitions of functions, but they do not have a body. The equals is missing, and the body is missing. And that's permissible as long as the class is abstract. If I would remove the abstract I would get a problem which would say class inset needs to be abstract since method contains is not defined and method include is not defined. So let's add the abstract again. So you've seen that abstract classes can contain members which are missing an implementation. On the other hand, we get instances of an abstract class with the operator new. So if you would try that. Write here new IntSet. I will get an error which says that the class IntSet is abstract and cannot be instantiated. So since IntSet is incomplete, how would we implement it? Well, one way to do that would be using the binary tree data structure. In the data structure there are two types of possible tree. A tree that represents the empty set, and a tree that consists of an integer and two sub-trees. And the idea would be that you keep always the invariant that the trees are sorted, so that means that, if I give you an example, here we would have a tree let's say with a 7 and that would have two sub trees. And here we would maybe have an 11 and here we would have a 5, and then the 11 could have a 9. And the rest ones would all be empty. So that's an example of a binary tree. And the invariant that we want to maintain is that for each node, the nodes on the right hand side of this node all have integer values that are higher than the node, whereas the nodes on the left hand side all have values that are less than the node. That this will make it easy later on to implement the contents test that determines whether a given value is in the tree. So let's start with the empty tree here. The empty tree is very simple to implement, we would simply say that contains is always false, because the empty tree cannot contain any value, whereas include is a method that returns an IntSet. So what set would it return? Well, it would return a NonEmpty set to be defined that contains the given element and two empty sub-trees. So going from the empty set to an include set would give us a set like that that contains a single node and two empty sub nodes. Let's put that class into the worksheet. Now, it still remains to define what a NonEmpty set would be. A NonEmpty set in that implementation would be represented by a class NonEmpty that takes an element that's the integer stored in the node and the left and the right subtree, which is in each case an IntSet. And the implementation of contains and includes now makes use of the sorted criteria of trees. So that means that for contains we always have to look in one of the possible sub-trees. If the given number x is less than the current element value then we know we will have to look in the left sub-tree here, whereas if it's greater, we know we will have to look in the right sub-tree. If it's neither less nor greater than it must be equal and in this case we have found the element. The include method follows a similar algorithm if the element to be added is less than the element in the tree then we need to include it in the left sub-tree, if it is greater we need to include it in the right sub-tree and otherwise, the element is already in the tree we can return the tree as is, there's nothing that needs to be added. One important aspect here is that we're still purely functional, there's no mutation. So when I say we include an element in the left sub-tree, what I really mean that we create a new tree that contains the previous element of the tree, and a larger left sub tree where x is included in the previous left sub-tree, and the current sub-tree on the right. So, visually if I would say include the number 3 in the tree that we see here then what would happen is, I would create a new node 3 with two empty sub-trees. That sub-tree would be the left sub-tree of a new node with element 5 and an empty right hand sub-tree. And finally, my tree would be a new tree with as before the note 7 as the node and the same as the right hand side tree here. So we see that now, we really have two trees, the old one and the new one after the include. The two trees share the sub-tree here on the right hand side but they differ in the left tree where on the new tree, we have a 3 and a leftmost bottom corner, where as for the old tree, we just have two empty trees. On way these data structures are called, they're called persistent data structures. Because, even when we do changes, Or so called changes in quotes to a data structure, the old version of the data structure is still maintained, it doesn't go away. And persistent data structures are actually one of the cornerstone of scaling functional programming up to collections and the like. So let's take the definition of class NonEmpty and also add it in the worksheet, so that we can test something. So now, instead of new IntSet, I would create a set by saying, well this would be maybe, new NonEmpty of three, new empty, new empty. So it will be my first set, and it would give me a non empty set, and I then could write things like includer four, and that would give me an InSet which is another NonEmpty set. Now, before we go further, let's add a two-string operation to our sets so that we can actually see what sets we have created here. We need two definitions of two string, one in empty and one in non empty. For empty, how would we define two string? Well let me just define it in a very minimalistic way to give you a period. For NonEmpty, I want to define toString as follows, I want to say it should be an open brace followed by the description of the left element, followed by the sign, as follows. It should be an open brace followed by the string value of the left sub tree. Then the element in the middle, then the right sub tree, and then the closing brace. If we do that, then we see that here we get a sort of visual representation of our trees, so the first t1 would be the element tree, and an empty tree to the left and right. Whereas t2 would be the root of t2, the t2 would be three. The right subtree is a tree that has 4 as its element. So we see that this tree in particular has maintained the invarium that trees are sorted. So one new thing you see here is that both empty and NonEmpty extend IntSet. So extension means that these two classes are subclasses of the base class IntSet. One consequence of that is that the types empty and NonEmpty conform to the type IntSet. What that means is that an object of type empty or NonEmpty can be used wherever an object of type IntSet is required. Some more terminology. We say IntSet is the superclass of Empty and NonEmpty, and Empty and NonEmpty are the subclasses of IntSet. In Scala, any class that you can define extends another class. Even for a class like rational, where you didn't give an extends clause, there is a superclass. So if no explicit superclass is given, the standard class Object in the Java package, java.lang is assumed. Object is the root class of all Java classes, and also of all user-defined Find Scala classes. If you take the direct superclass of a class, and then it's superclass, and so on. So all the direct and indirect superclasses of a class C, and these are called the base classes of C. So for instance, the base classes of class NonEmpty are IntSet and Object. So what you've seen in the example is that the definitions of contains and include in classes Empty and NonEmpty implement the abstract functions in the base class IntSet. They were not defined in IntSet, but they implemented in the subclasses, empty and not empty. It's also possible to redefine an existing, non-abstract definition of a superclass, in a subclass, but then you have to use override. So in this example here, you see the difference. If you write in the base class foo equals 1, and then you redefine foo in the subclass, which extends base, then you have to put an override. So let me demonstrate that to you in this worksheet. We have two classes, a base class and a sub class, and each of these classes have two members named foo and bar. The bar member in class sub, implements the abstract definition of bar in class base, where as the foo member replaces the concrete definition of foo in class space. If a member replaces or overrides a definition in a super class, then the override here is mandatory. So, let's say I leave that out, compile, then we will get an error which says overriding my method foo in class base. The method foo here need an override modifier. So why is that, why here is Scala is more picky than Java. Well, the reason is that sometimes you do not really know what methods you do inherit, so you might actually accidentally override and mess up in a base class. And in that case its good that you have to be explicit about it. There's an added benefit mainly when you intend to override, but lets say you can't remember the name correctly or you get the number of parameters wrong or things like that. So, if I write something like that, override the foo2, then I get an error again, where we say, message foo2 overrides nothing, so in that case the override will give me the opposite protection, which says when I write override then I want to be sure that in fact I do override another definition. For methods that implement definitions in base classes the override is optional. You can write it if you want to, and in that case you will be warned in the same way, so if I right bar2 I will get an error as before. But you donâ€™t have to do that, so you donâ€™t need to write the override, you could just write def bar and thatâ€™s usually less noisy. So prefer. So let's get back to the IntSet example. One thing to improve would be the status of the empty IntSet class. You could argue that there's really only a single empty IntSet, so it seems overkill to have to use a create many instances of it. And in fact that's very easy to achieve in Scala, we just use an object definition instead of a class definition as before. So this object definition is exactly the same as the class definition, except that instead of the keyword class, you use object. And that will define a singleton object named Empty. There is only one of them, and you don't need or can create that explicitly with new. So the object Empty simply exists. Technically it will be created. The first time you reference it. You reference to the object simply by naming it with Empty, so Empty here and here replaces the creation of New Empty that we had before. In terms of evaluation, singleton objects are values so empty is already a value there's no evaluation step that needs to be preformed. So, since we're already looking at objects, let's take a quick excursion and look at what a program would be in Scala. So far we've executed all Scala code from the REPL or the worksheet, but it's also possible to create standalone applications. Each such applications would contain an object and that object contains a main method. Let;s look at this with the mandatory Hello World program with name, scala. So, you would drive an object, name it Hello. It has a main method, then, as in Java, that main method will have to take an array of strings. You can ignore that for the time being. And the body of the main method would be a printIn with hello world. Once you compile this program, you can start it from the command line simply with scala and then Hello. Or you could also use Java and Hello, that would do the same thing in this case. So let me demonstrate that in Eclipse. So we create in the week three package this time not a worksheet but a Scala object. Call it Hello. And then we have to add the main function, which takes this. And it would Print something. We can run this program as a program using the green arrow here, so we run it as a scala application. And we see on the console, hello world. So that's how you would run full programs in Skylight from Eclipse. So let's do an exercise. The task is to add a new method to our IntSet class hierarchy. The method should take the union of a set and some other set. So you should extend the class IntSet to be the following abstract class and implement that. So how would we go about that? Let's first start with the abstract class. So here's the signature of union, and now we get two errors because we have to implement it in the two sub classes. So let's start with empty. What would the union of an empty set and some other set be? Well, that's very simple. Empty set union some other set is always the other set. So for non empty set, how would we define union in this case? Well, the idea is that we split a set into its constituents. So we have the left IntSet, the right IntSet, and the element. Let's form the union instead from something that is smaller. So one of the subsets. Let's start with left and do union right. So that would give us the set without the initial element. Then let's finally give the union of the other set. And as a last action, let's include the element back into the set. So the new union has all the parts of the previous ones. It contains the left set. It contains the right set. It contains the element. And it contains the other set. So it's very easy to convince ourselves that indeed the result set here would have all the elements of the union. And would have no other elements. But that's another concern, and that is, well, how do we know that this recursion actually terminates? Because, after all, union calls union and we've seen already cases where calling yourself again would not terminate it. So the argument here we could make here is to say, well every call to union is actually on something that is smaller than the set we started with. First call to union was on the left sub-tree here. And obviously, that set is smaller than the whole non-empty set. The second call to union was on the union on left and right. And that again contains one element less than the full set that we have here. Because every call to union is on a set that is smaller, it follows that at some point we'll have to reach zero. And if the set is zero then we fall back to the case of empty sets where the union is immediate, we just returned the other set. So that's how we convince ourselves that the union that we operations that we do here are in fact terminating, and once those are terminating, we can safely add element back with an include operation as before. What object oriented languages do in this case, scala included is to use the dynamic method dispatch model. That means that the code that's invoked by a method called depends on the runtime type of the object that contains the method. I believe that's best shown in an example. So let's say you have the code Empty contains 1. What do you do? Well you look up the contains method in empty. Remember that was an object empty and we had a def contains y equals false. That was it's definition. So looking at that method and performing the necessary substitutions, we get false. Here's another evaluation using NonEmpty. Would have the set NonEmpty(7, Empty, Empty)) and we ask whether it contains 7. So if we look back at the definition of contains, in NonEmpty, that's what we see here. And if we apply our substitution rules, then it means that this call of the method will be replaced by the right hand side of the contains method. Which you see here, and at the same time, the substitutions of the parameters of the class and the method and the self reference. So, the three elements that we see here. If we perform the substitution, we arrive at this expression here. So, instead of x and elem we have, we see 7 in each case, and instead of the this we see new NonEmpty(7, Empty, Empty). If we perform the simplification, we get that 7 is neither less nor greater than 7, so we fall into the else case, and we get true. So for the end of this session, here's something to ponder. You've seen that dynamic dispatch of methods and object-oriented languages is actually quite similar to calls of higher-order functions in pure functional languages. The similarity is that in both cases the code that gets executed on a functional method call is not known statically. It's not apparent from the name or the type of the thing you called. But it is determined by the run time value that is passed. So it's a fair question whether we can implement one concept in terms of the other. Can we maybe implement objects in terms of higher order functions? Or can we implement higher order functions in terms of objects? Or can we maybe go both ways? It's a question that is fairly open and that has a range of possible answers. So I just left you with that. Think a little bit about it and maybe discuss with others.