[MUSIC] In this segment, I'm going to show you a third implementation of rational numbers that will be equivalent to our other two, as long as we keep the type abstract. And what I'm going to do here, is actually change the implementation of the type rational, in order to emphasize that when the type is abstract, an equivalent structure can go ahead and change how the type is implemented. So, given a signature that has an abstract type, different structures can have that signature while implementing the type in different ways. Those structures might or might, might not be equivalent. I'll show you an example where they are. So I'm about to show you a third implementation, rational 3 And in rational 3, the type rational is going to be implemented as a type synonym with int * int. So let's go over and see that. Here is my structure. You'll see I have a type synonym here, so in the rest of this module, rational and int * int are the same type. The outside world, however, doesn't have to know that. So before I show you these functions, how about I just show you, remind you, what the three signatures look like? Now signature RATIONAL_A requires the module to implement rational as this data type. Since our module does not, rational 3 does not have this signature. If we try to give it this signature, the type checker will reject it. But it does have this second signature, RATIONAL_B. As we'll see, we already saw it defines a type rational. It defines it to equal int * int. That's a fine way to implement a type rational. But, given that definition, it's going to have to have these three functions, where rational is int * int. The outside world, as usual, will not know that rational is int * int, and then for RATIONAL_C it can have this signature, provided it has everything it had before, as well as a function of type int arrow rational. And I'll show you that at the end. So, let's see how struct rational 3 is implemented and the fact that it can have type, the signature RATIONAL_B. So rationals are now just pairs of ints, in all cases. So here's make_frac. If the denominator is 0, raise an exception. If y less than 0, then produce (-x,-y). Just like before, but now it's just a pair of ints, there's no frac constructor involved. Otherwise return (x,y). Notice we're not treating whole numbers any differently. So if this denominator is 1, then we'll just, return (x,y). Okay? To add 2 fractions, well, add needs to take 2 rationals. And each rational is an int * int, so this pattern will do well. And, we're not going to reduce things. That's an orthogonal choice, but like Rational2, we're just going to wait until toString to reduce things. So we'll just return a*d + c*b in the first position, and b*d in the second position. And then finally, toString is a little more complicated, because we still want to return everything in reduced form and treat whole numbers specially here. So here's what you need to do, if x is 0, you have a numerator of 0, you better return the string 0. Otherwise We have some more work to do here with GCD and reduce. so here's a let fun gcd equal this in, let's convert to a string the numerator concatenated with if the denominator is one then don't do anything. Else, a slash, and then to string of the denominator. So, I didn't empahasize this. Up here, I've computed the num and the denominator, by dividing both by the gcd, of the absolute value of x and y. So, a bunch of arithmetic, but the point is, to not print the denominator, if the, if the numerator is 0, or if the denominator is 1. And so overall, if we go back up here to the signature RATIONAL_B, we've provided everything at the right type, make_frac returned an int * int, add took two int * ints and returned an int * int. And toString took and int * int and returned a string. And then the outside world does not know that rational is int star int. Now, if we want to implement Rational3, we also need a function, of type int arrow rational. This was this cute thing where, for our other structures, the data type binding was providing this function, and then we are exposing just that part of the data type binding to the outside world. Now, in our new structure we don't have a data type binding, we don't have such a function being defined by it, but we can still implement this signature provided we have this function. And so, again, it's a little cute, but down here at the bottom, I just defined a function whole, you're allowed to start functions with a capital letter if you want to, that just takes in an i and returns this rational, this int * int of (i,1), and it works great. So, that is our third implementation. I want to emphasize, that when we do signature matching of this structure, against either RATIONAL_B or RATIONAL_C, a couple of interesting things are going on. So let me emphasize those for you. the first is make_frac internally has type int * int arrow int * int. And so that does match a signature where we're exploding, exporting it as int * int arrow rational, because rational is int * int. And the client will never be able to tell that we're actually returning something of the same type we're taking in, because the client doesn't know those are the same types. Now what's interesting, is we could If we went back here to RATIONAL_B, we could say that make_frac, instead of being int*int arrow rational, it's actually rational arrow rational. Now this is a very bad signature. The structure has this signature. The type checker will accept it. But, the structure will be useless. Because if all the outside world knows is that it can use these bindings, it's never going to be able to call make_frac, add or toString, because it can never get its first rational to get started. There's no way it can call any of these functions. So there are programs out there that type check and are not useful. Okay? So we really want to expose here that make_frac does take 2 ints as arguments. So that's the first interesting thing. The second interesting thing is this function, whole. So let me go back and show that to you here at the bottom. Based on what we know from type inference, this is going to have type alpha arrow alpha * int. And that is indeed what the type checker will give this function internally, when it goes to type check it. But in our signature in RATIONAL_C, we said this had to have type int arrow rational. And so somehow, the type checker, which is fairly sophisticated and fairly smart, has to get from this first type to this second type legally, and it turns out it can, because the rules of signature matching let you take a polymorphic function and instantiate it to a non-polymorphic function. So the first thing the type checker has to figure out to do, for signature matching, is to realize that alpha can be int, and therefore, this could also have typed the less flexible type, int arrow int * int. We have to replace all the alphas consistently, and now we see, that indeed, since rational inside the module is int * int, we can, as we usually do, match this type against this type in the signature, and its pretty neat that ML does that for us. Notice that this does not have something like the type, alpha arrow int * int, or you know, int arrow alpha * int. When you take a generic type, a polymorphic type and make it less general, you have to replace all the alphas with another type. The, this function hold does not have these bottom two types, that doesn't make any sense. It is not the case, that if you called whole with a string, you would get back an int * int, so these types are just wrong and I will delete them. It is interesting to me that inside this module, we actually could call whole with a string for i, but outside the module, because of what the signature says, we can only call it with an int, and therefore it will return irrational. Okay? So, that is the more sophisticated and interesting details of this third structure. But the high level point is that under RATIONAL_B or RATIONAL_C, this structure is equivalent to our other two structures, even though it implements the type rational and in, and in an entirely different way.