 Hello, everyone. This is a lecture for CSE 340. This is a makeup lecture for the class of 1014. And today, we're going to be talking about Hindley-Milner type inference. So up until this point so far, we have been talking about type systems. So we've seen all the different ways that types can be described and that a programmer or a language designer can design the type system and what kind of choices that they have. But in everything that we've seen so far, the programmer must declare the types of the variables. So we've seen that the programmer can, let's say, declare an array of length 6 of 0 through 5 of an int and call that a and can define an int i. And then later, when the type checker is operating on the program, if it sees the bracket operator, so an array access operator, and it's setting a i equals 1, the type system knows from the declarations, hey, this is acceptable because the bracket operator takes as the internal i takes an integer, so i is an int. That's good. And the right-hand side is an integer. And the left-hand side, applying the bracket operation on an array, returns the type of that's inside. So that is going to be an int. So we have an int is equal to an int, so the types all match here. That's good. And then the compiler can find other kinds of scenarios where, let's say we declared i as a string this time. Well, now, when we apply the bracket operator to say a i equals 1, now we have a type error because we know that the type of i in between the bracket, the type of that expression, must be an integer. But here the type of that is a string. And we can see that here clearly because the programmer has explicitly declared the types of these variables. So what if, maybe instead we have a similar case where now we do have an int i, but now we're trying to set a bracket i is equal to testing. So this will also not compile because the programmer has declared that this array a has the type of array of integers. And this is essentially then trying to assign a string to an array of integers. So this will not type check and that will throw a type error. And there are more expressive type systems that maybe you're actually familiar with in the programming that you've done so far. So one thing that we're gonna talk about is parameterized types. So some languages actually allow the programmer to declare essentially a type as a parameter to either a function or a class. And so instead of the function or the class being specific to a given type, the type is given as a parameter when either that class is instantiated or when that function is called. And so a lot of modern object oriented languages actually are going in this direction. So Java and C-sharp both have generics that allow this. C++ has templates. One interesting thing is how these languages are evolving over time. So it's actually kind of interesting for me personally when I first started programming in Java was around the time where generics were very first introduced. So that was like a brand new concept in the language. And so it was really interesting to try to see that transition where before a list just had, it was a list of objects. And you had to be very careful about casting when you put items into the list and casting when you took items out of the list. But now with generics, we can actually define a list class that operates on any type that we want. And so let's look at an example of this. So this is Java code. So this is, I believe, Java eight code. Just in case you wanna try this at home on your own computers. So what we're gonna do is we're gonna define a class called the chooser. And this class itself doesn't have any type parameters. But we're gonna define a new random from the Java util random. And then we're going to define a function called choose and this function has a parameterized type. So here we can see the Java syntax. So we're declaring a public static method called choose. And we know that it's parameterized types because of the angle brackets with the T in it. So now we're telling the compiler, hey, the type for this is any type T as long as the return type of choose is the same as that T and choose has two parameters. The first parameter has type T and the third parameter also has type T. And so here we can define a method that's very generic. We can define a method that let's say randomly chooses either the first or the second argument. So here's kind of the code to do that is we're calling random.nextint which returns a new random integer. And we're modding that with two and then say, hey, if that's equal to zero then return first, otherwise return second. And so now we can actually use choose in many different scenarios. So now essentially you can think of that type T as a parameter to our function choose. And so when we call choose, we can pass in and say, hey, let's call choose with ints and that way I know to pass in first and second as integers or I can call choose with strings or I can call choose with really any object or any type I want. And that's because this function choose isn't specific really, it doesn't matter what the types of first and second are as long as they're the same. That's the really important thing here. So we can actually use this choose function. We can define a class and then we can define a main method of that class using the standard Java boilerplate. Then we can declare two integers. Let's say int x is equal to 100, int y is equal to 999. And then we can print out, hey, choose.choose x and y. And so what this will do is at runtime it'll print out one of either x or y, either 100 or 999. And then we can define a string, two other variables that are different types. Let's say string a and string b, foo and bar and then print out choose.choose either a or b. So this will print out either foo or bar. And the really interesting thing here is in kind of older than Java 8 versions we had to actually tell the compiler which choose the parameter to that type t. We would say choose.choose bracket and we put in int here. So it knows that we explicitly wanted to do integer. And in this next one we put string and say, hey, we're specifically passing you string objects. But here as of Java 8 Java is a little bit smarter and it's able to infer based on the types here, what should the type of this t parameter be? And so if you can put this program in, you can run this program and you'll see that the program actually will randomly print out either 100 or 999 and then either foo or bar. And every time you run it, it has the potential to output different types. And so this is an example of a parameterized type. So this choose function is a parameterized type and you can of course extend this by specifying types t, u, v, whatever you want. But so this is actually a form of what we call explicit polymorphism. So in the previous example, we had to actually declare the parameterized type specifically, we had to declare, hey, we are using this type t and this type t is being used and first the parameter first has the type t and parameter second has the type t but we as the programmer had to explicitly declare that. And I should briefly note that this is a slightly different concept of polymorphism than what usually think about an object orientation concept although the underlying concept is very similar. So here we have explicit polymorphism in that that function is polymorphic and it will adapt to whatever types t you pass in for its parameterized types. And so this way you can have a function that can be called with different types but the really important thing here is that the compiler can still check for type compatibility. So the compiler can make sure that the types are checked statically before the code is executed. So you get kind of some nice benefits. You get the benefits that you can write a function or a class once and it can be essentially be applied to any of these types and you get that checking at runtime to make sure. So for instance, if I had tried to call chooser.choose from the last example if I had called it with an integer argument of integer variable and a string variable I would get a type error that says, hey, this doesn't satisfy that type because here there's no single type t that satisfies both the first parameter and the second parameter. And so this is great because you get that type compatibility but there is actually a very cool alternative to this. So this is really cool but we had to specify exactly that type t, the type t and where it was used in the parameter types. So there's another type of polymorphism that we call implicit polymorphism. And here the programmer does not need to specify the type parameters explicitly. Now, you may be thinking, well, but this dynamic, I don't have to, when I program at Python I don't actually have to dynamically declare the types either. So does Python have implicit polymorphism? And while dynamic languages obviously do have this property too in that the programmer doesn't need to specify the type parameters. The key here in implicit polymorphism is the type checker will statically before it ever runs the code, attempt to assign the most general type to every construct in the program. And so this means that the developer doesn't have to write that, hey, this function is a generic function and has these type parameters. The type checker will actually be able to automatically infer from the definition of that function what the most general type is for that function. And this is actually very powerful because it can do it to all points in the program. So you kind of get in some sense the best of both worlds in that you get, you as a programmer don't have to explicitly define types, but you get the benefit of static type checking so that if you try to call a function and you're violating the types, well then you will, if you call a function and you're violating the types, then you'll get a type error and you'll get that statically rather than dynamically, which is what you get at runtime. So in a scripting language like Ruby or Python or Perl, you get that type error when that code is executed. But implicit polymorphism is very cool because specifically you don't have to define the types although you can, but for all the types you don't define, the system will automatically try to infer the most general type to every construct in the program. And so this is something, so we're gonna actually go into specifically how this is done and the algorithm that's used to kind of calculate this, but this is like a really cool concept. So we're gonna go into some examples of implicit polymorphism here to try to first look at it, look at a function and try to say, can we guess or try to infer ourselves what the most general type is here? And then we'll eventually learn the algorithm for that, but I want you to kind of maybe you can pause this video and before I reveal kind of the answers, you can stop and ask yourself, okay, what is the most general type here? And does what I have appear on screen, does that actually make sense as the most general type? So one note before we start, so I'm gonna be giving implicit polymorphism is really prevalent in, even though it's gaining ground in a newer, more traditional object-oriented language, it's really popular in the realm of functional programming languages. And so I'm gonna be giving, these examples are gonna be given in a functional programming style, not really any one specific language, but it should be very clear to follow along and there shouldn't be too many questions here, but you'll see how you can apply these techniques to a C-like language, which is what you'll see in the next project. So let's look at our first example. So here we have a function foo that takes in a parameter x and we're defining it as x. So here we're defining a function with the keyword fun because that is really fun, we wanna have fun. And we're saying that foo takes in one argument x and here as kind of the functional style, we're leaving off the return keyword. So basically whatever the expression on the right returns, that's what our function is going to return. So the question is, well, what is the type of foo? So you can see here, we haven't defined any types. We haven't given a type to foo. We haven't given a type to x. Fun clearly doesn't have a type, it's a keyword. The equal sign, this is just a way, the syntactic way of how to define a function. So really, what is the type of foo? And so here's maybe where you could pause it to try and think for yourself. Well, what would be the type of foo? And you can think about it with the types that we've already, we talked about in the type systems lecture. So we will define, so the type of foo from looking at it is pretty easy. Well, it's a function of t, so it's got one, we know it's got one parameter, and we can give that parameter some arbitrary name. It doesn't matter what it is. We'll stick with kind of a convention of using t, unless it doesn't make sense. And it takes in, it's a function of t that returns t. So that's really what this function, what the type of this function is. And this is the most general type of this function. So we can apply foo to any argument. It doesn't matter. And whatever the type of the argument is, well, then that's gonna be the return type of foo. And that's very easy to see because this is just the identity function. Foo is simply returning its argument. I will introduce a new kind of syntax for defining types, and this is taken from kind of the O'Cammell style, where we can represent the, so the same thing. So this is semantically equivalent to a function of t returns t. Here we're just shortening it and saying, we're gonna have optional parentheses, but we'll have the types of all the parameters separated by commas, where there's more. We'll see that later. And then an arrow, a dash and a greater than symbol, and then the return type of foo. So this is just a more succinct way of representing what we're representing here. So we'll use kind of both throughout. But you should be familiar with both. So now we can look and see what happens. Well, okay, let's say we've defined our function foo, and then we define a new function bar of y, and we define that to return foo of y. So now it's really interesting here. So now we have, we have a new function bar, and this function bar takes in one parameter, and what it returns is the result of foo with that parameter given as a parameter to foo. So now we wanna ask the question, well, what is the type of bar and the type of foo? Just from looking at it. So it should be pretty clear, the definition of foo hasn't changed, so therefore the type of foo shouldn't have changed, right? So we know that foo is a function of t that returns t, and so in our simplified syntax, we can see it's like this. But what is the type of bar? So if we think about it, well, what does bar do with its parameter? Well, it's passing its parameter to foo, and is there any constraints on what the type of foo is? No, not really. Foo will accept any type and return that type. Well, the return value of foo is gonna be returned for bar, so kind of by reasoning about it and thinking about it, we can say, well, yeah, bar is also a function of t that returns t. So bar actually has the same general type that it takes in a t and returns a t, and for any type t, it doesn't matter. And so this is kind of one example. So let's look at some other ones. So this is where it gets a little bit interesting, a little more interesting. So here we're gonna define a function called max that takes in two variables, x and y, and says if x is less than y, then y, else x. Now, if you're not super familiar with functional programming style, this may seem a little strange to you, but conceptually, well, in essence, you can think of an if as an expression that returns something. So if the condition is true, then whatever's in the then block is going to be returned. Otherwise, if the condition is false, whatever's in the else block is going to be returned. And of course, if we were defining a whole language, we would define what happens if there is no else. So certain languages actually allow you to do this. If you're familiar with Ruby, Ruby allows you to do this and actually makes programming pretty nice because you don't have to have explicit return statements in any of these clauses. And you don't have to have like a ternary operator. You can use this as the ternary operator. So, but here we're concerned with, okay, what's the type of max? So remember here, we're trying to get a kind of intuitive understanding for what's the type. So let's kind of eyeball this. So maximum is going to, so the max function takes in two types, x and y, and then it compares them with the less than operator. Excuse me. It compares them with the less than operator and says if x is less than y, then return y, otherwise return x. So we can see that we're returning y and or x. So that means the types probably have to be the same, right? So, because our return type has to be consistent. So if we're returning either y or x, that means those types must be the same. So we don't have two types here, we have one type. And we can see we're using the less than operator. So to compare them. So really it depends on the semantics of this less than operator. But let's say it only works on integers in our language or ints. So that would mean that for this program to be correct, x and y have to be integers. So then we know that the type signature of max would be a function of int comma int that returns an int. So we know these are both integers because they're both used in the less than comparison. And we're returning an integer because each of our branches in the if statements returns an integer. And so we can write this in our simplified format by in this way, so by bracket int comma int arrow int. So this means the type of max is a function that takes in an integer, that takes in two parameters, an integer and an integer and returns an integer. But so as we can see, so even though the even though the implicit polymorphism is trying to infer, I mean, we are essentially performing the role of a type checker, but we're trying to infer the most general type here. But you can see here, this type is not very general, right? Before we were looking at types of T's, T's returning T's that could be applied to any type. But this max function is very specialized and it can only be applied to integers. And that's really because while we, this less than operator that we're using here, right, that less than operator is actually, sorry, that less than operator is actually being, is constricting our types that can be applied and say, okay, well X and Y have to be whatever types can be applied to a less than operator. But if we wanted to be more generic, right, maybe is there some other way we could write the maximum, the max function such that we can provide the comparison. So, what we can do is now we can let's say define a function max and pass in a comparison function here called CMP, pass in a variable X and a variable Y, then we can say, well, if compare X, Y, so if that returns true, then return Y, otherwise return X. So now that we made this change, now what is the type of max? So if we look at this, we can see, we can kind of think, we can kind of use our same reasoning before and say, well, okay, so we have two branches of it if the then block and the else block, the then block returns Y, the else block returns X, that means these types have to be the same, right? So we know that for sure. Let's see, what are the things that we know? So we know CMP is a variable that's passed in, it's applied to X and Y, so that means it's gotta be a function and it's gotta be a function that takes in two parameters and those two parameters have to be the same type. Remember, we just said that Y and X are probably have to be the same type. But what is comp return? Well, what's valid for an if statement to compare? Well, it's gotta be a boolean. I mean, it depends on the language. Once again, which we've talked about a lot in this class is that a lot of these answers say it depends on the language. But let's say we have a specific variable type and that whatever's in the condition of an if statement has to be a boolean or a bool. And so, well, what's the type of max? Well, max is a function, we know that, we can see the definition. It's first parameter, so it's gonna be a function and it's a function that takes in three parameters. The second and third parameter are the same type, so type T and it returns T. So we know that it's gonna return whatever's the type of the second and third argument. But the first argument, that comparison function, we were kind of able to deduce by looking at this, hey, this is actually a function of T and T that returns a boolean. So this is a function that takes in type, two variables of the same type and returns a boolean. And so we can represent that in our shorthand syntax of the function of max is these outer parentheses. First parameter, a function that takes in type T and T and returns a boolean. Second and third parameters type T and max itself returns a T. So is this more general and more applicable than the function that we wrote in the last slide? And yeah, you should be able to see that clearly by just looking at this, right? So the functions here, so previously, our function was specialized and would only work to compare integers, whereas here, we now have a, we're able to pass in a function to use as a comparison between the integers. So as long as we pass in some way to compare the functions and some, and as long as we pass in a function to compare our parameters and pass in the parameters, then this will work. So what's great about this is now we have this really generic max function that we can, let's say, call max and pass the less than function, which, you know, less than if you think about it as a function, is a function, right? That takes in an int, takes in two parameters, an int and an int, and returns a boolean, right? Whether one is less than the other one. And then we can pass in 10 and 200. And so here, this will, this will give us 200. So there we have an awesome way of, so now we, this gets us the same functionality as the previous max, but with that same function that we defined, now if we wanna compare strings, well, all we have to do is pass in the string comp function and this will give us the strings, so this is defined on, let's say two strings and returns a boolean. And now we can pass in the strings foo and bar and the same thing, the max function can now be applied to strings. So this is kind of the cool thing and notice that we didn't have to define that in the definition of function max. We're actually able to infer the types here of the various variables. Okay. So now let's look at another example. So now we have a function foo that takes in three parameters, a, b, c, and the result of foo is c getting called on a bracket operator b. So now what's the type of foo? Okay. So this requires some noodling, so let's kind of work inside out. So we can look at this array access and we can say, okay, well this means a must be some type of an array, right? And we know that b must be an integer because you can only have integers to access array elements here. So we can say, okay, we know this is some array of some type t and we know that b is an integer. And we know that c is, well, c is a function it takes in, so we know that the array operator, the bracket operator will return the type of whatever's in the array. So if we say that a is an array of t, then we say that, well, c takes in a t. And now the question is, well, what does c return? Does c return a t? And that may be a kind of your first guess. Well, yeah, it can return a t. And that's true, but it's not the most general thing that c can return, right? So we're trying to be as general as possible. And there's no constraints here right now that whatever is passing to c is the same type as whatever is leave c. So we can define the type of foo as it's a function of three parameters. First parameter is an array of t's. The second parameter is an integer. And the third parameter is a function that takes in a type t and returns some other type u and foo itself returns u. So in the format we've been using so far, simple cation format, here's what we have. And so you can see that by saying that, well, t and u can actually be different types that's actually more general than if t and u are the same types. So this function foo, so we can call this function foo pass in an array of whatever. Pass in an integer and pass in some function that map that takes in parameters of one type and returns something and then we can call this function foo. And we're able to automatically infer this type specification without explicitly defining any of the types here, which is really cool, right? So we're actually able to infer that b is an integer just based on its usage. Okay, let's look at one more example before we go into how to actually automate this. So we're gonna define a new function foo that takes in three parameters, a, b, and c. And we'll first, so we're kind of doing it a little bit differently, but here we're gonna set a to be 10 and then we're gonna call a of b of c. So now the question is, what is the type of foo? So we can kind of look at this, go forward and backward. We can kind of go inside out like we've been doing. We can see that, well, c has to be an integer, right? Because it's used in the brackets. b has to be an array of some type t and a needs to be a function that takes in a t and returns some other function, some other type u. But now when we look at this line, we say, wait a minute, this line says that a has to be an integer. That's strange, but here on the second line, this line is saying that a is actually a function. And so actually what the type, so this is a type error, right? So we have, we're using a as both a an integer and as a function and it clearly cannot be both. So here's the case where our implicit type check, the implicit polymorphism, the type checker here would say, ah, this is the type error because look, you've constrained that a is equal to 10 on this line. So a is an integer here, but here you're using a as a function. So it can't be both. So this has got to be a type error. So Hindley Milner type checking is an approach and an algorithm of how to do this, of how to actually generally to infer types generally. And it does this really in three ways. So the first way is it infers the types of constructs that are not explicitly declared. So we'll see that in a moment, but it's pretty easy to define for the language, kind of like we've been doing, right? So we've been saying, well, if you assign in, you know, if you're in a Boolean if statement, that means you're a, or if you're in a if condition, that means you have to be a Boolean. And then it takes and it leverages the constraints of those various constructs. And then it applies all these constraints together with type unification, which we're gonna look at to a second in order to find the most general type for each construct, or it can find a type error if there is one. And so Hindley Milner has a lot of other benefits. It's very fast, but we kind of won't go into the theory or any kind of those kinds of stuff here. But it suffices to say, so this is something that definitely used to be on the fringe of type checking in academia, but now full Hindley Milner type checking is used in real world languages like OCaml, F-Sharp, and Haskell. So if you wanna play around with this kind of stuff, I urge you, you can go check out these languages. OCaml is a variant of ML. F-Sharp is by Microsoft. I think it runs in the Visual Studio environment. They're all functional languages. And Haskell is a ML-like language, but has some significant differences that also make it cool and unique. So if you're interested, I urge you to try these out. I mean, you'll be getting familiar with Hindley Milner type checking through the homeworks and through the projects. But it actually does help a lot when using these languages. So just as a small anecdote, so I did a research project in OCaml that you can find on my GitHub if you're interested. And I didn't just choose OCaml because I wanted to choose something interesting and crazy. I did it because I wanted to do static analysis of Ruby on Rails applications. And there was already a research project that somebody had written to actually create the abstract syntax tree and do all the lexing and parsing of Ruby code. And so I was like, hey, well, you don't want to have to invent all this yourself. So I wanted to leverage that library. But the library was written in OCaml. So I thought, hey, well, it's a good excuse for me to use OCaml and learn that language. So by doing this project in OCaml, I can define the type system as really kind of interesting. It was definitely a steep learning curve at first because you're not really used to defining all these types or not actually, you're used to not, you're not used to not defining types, but you're also not used to the compiler yelling at you if the types don't match. And so oftentimes you have to like debug these, A, A, B, C, returns, D, returns, C, integer array type errors. So it does take some getting used to, but what was really interesting is when I'd go to refactor the code. So at some point you're like, ah, I'm getting this information, but I need that information plus some count. I want to keep track of, I don't know, how many if statements there are or something silly like that. So that when you start making these changes, once your program type checks, it's actually highly likely to be correct. So it's more work to get it to the state where it can type check. But once it does, it's highly likely that it's correct. And so it will actually save you in a lot of places by saying, oh, well here you're returning this tuple of an integer and a string, but here you're only returning a string. So that type checking doesn't match. So then that's you going, oh, I didn't extend that to change that from an integer to a tuple. So now I can actually keep track of more information. So that's just my personal aside about why I really like this idea and this approach so much. Okay, so let's, so, Hindi Milner type checking has, we have to first talk a lot about constraints. So we're gonna define some constraints to our language. And these should be, you know, the sound may be kind of scary, but they're actually very straightforward. So the first thing we can find is for constants, right? So we know the type for any constants that the programmer uses. So if they use any constant integers like, I don't know, negative one, zero, one, two, whatever in the program, we know that the type of that has to be an int, assuming a certain language, we can say it's an int. The same logic goes for constant real numbers, right? So any floating point number that's constant in the program, we know the type of that has to be a real number. And the same thing with Boolean, any place we have a true or false, we know that that type has to be a Boolean. Finally, the same thing with strings. So anywhere we have a constant string or a string literal in our program, we know that that type has to be a string. So these are gonna be kind of the building blocks of the type inference, because we can use these types and we can see what constraints are applied based on how these constants are used and how our variables are used. But constants aren't the only things that we can actually use to infer the types in the program. We can also use all of the built-in operators. So for instance, for relational operators that take in an A and a B and apply some op to them. So for these, I'm gonna represent these as a tree. And this is the way you'll want to do this on the homework because it really helps thinking about, oh, this is actually, it helps to go through and apply the Hindi Milner type inference. So if I have some piece of my code that's A op B and we'll define what op is in a second, I'm gonna create a parent node called op, which has some type T1, I don't know what it is yet. And it's gonna have two children. One on the left-hand side is the A, which is left-hand side of the operator. And one on the right-hand side is the B, where A has some type T2 and B has some type T3. Now just as I described them here, T1, T2, and T3 have no relation at all right now. They just stand for any types. They are, you can consider them kind of the generic parameterized types. And so just to be clear, operator is one of these operators we're talking about, less than, less than equal to, greater than equal to, not equal equals. But the beautiful thing is, whenever I see an operator in this form, so I know there's three types involved. T1, T2, and T3. So I know from this that the semantics of my type system and my operators, that an operator always returns a boolean, right? And so I know, and once again, of course that's language dependent, but I know that T1 has to be a boolean. And, but I also have some other important piece of information. And this once again depends on the language, but I know that T2 and T3 have to be some numeric type. So I know that they have to be the same, first I know they have to be the same. And that's of course language dependent, but we want that restriction, because we want the programmer to be explicit if they're gonna compare different types of numeric values. But they're definitely the same type. And we know they're not strings, so we can use that piece of information. Similarly with the arithmetic operators, so here we have our same tree, we have our op A and B, and we operators either plus, minus, multiply, or divide. So what can we infer, what constraints do we know about the types T1, T2, and T3 whenever we see an arithmetic operator? So what we know is that they well, they have to be, so T2 and T3 should be the same type. And whatever they return should also actually be the same type, right? Because if I do A plus B, one plus two, an int plus an int, well that should return another integer, right? It shouldn't return a real number. And they should also be, let's say numeric in this language we're talking about. But we know at least they all have to be equal to each other, those types are equivalent. Okay, and there's one more operator we wanna talk about. So we've gone through it over into the examples, but we have the array access operator of A bracket B. So we're gonna represent this the same way we've been representing operations, right? So here we have the bracket operator and that return has some type T1, and it's left child. So in the other diagrams, the left child and right child didn't matter which was which, right? But here it does matter that the left child is A, it is the array that we're operating on and the right child is B, which is the index. So once again, here we have this situation where we have three different types, T1, T2 and T3. And what we wanna do is try to define some constraints based on the semantics of the array access operator but what these types can be. So the first thing we know, well, if we're applying the array access operator, the thing we're applying it to better be an array, right? So that means that T2 must have the type of array T1. And furthermore, we know that whatever we're using to index has gotta be an integer, right? We can't take real number indexes. We also, our language does a lot of strings. So we know that T3 has to be an integer. And the other important thing, maybe it's not clear cause I didn't make it clear but in the type of T2, right? We actually know that the type of T2 is an array of whatever T1 is. Right, so here we actually know and this is because, oh, the array access operator actually gets rid of this array of and returns something of type T1. So if we're already saying, hey, this is type T1, type T2, type T3, that means, well, this type T2 has to be an array of T1 and type T3 has to be an integer. And it'll probably be more clear in a second but A and B here, these left and right nodes can be arbitrarily complex expressions, right? It could be a function on the left here, A instead of being A, it could be a function call that returns an array. And on the right, it could be some aromatic math operation that calculates the index, right? So we don't care that B has type T3, we care that this node, the right child of the array access operator, that has to have the type int. And the left child of the array access operator has to have the type of array of whatever T1 is, okay? So, but we have other types of constructs in our app, in our program. We also have, we can apply functions, right? So we can call a function, we can apply a function foo to various arguments, x1, x2 all the way up to xk. So here, we're gonna create a parent and this parent is gonna be the result of the application. So we're gonna call this apply. And once again here, remember the order matters because we are giving semantic meaning to these trees and how their types relate to each other. So here, the leftmost child is gonna be the function foo which has some type F. Then we're gonna define, then each of the other children are gonna be the parameters, x1, x2 all the way up to some xk. And so when we have this, right? We want to think, okay, what are the constraints here of the types? Well, we have the return of this function application is gonna have the type r. The function foo that we're calling the leftmost child, let's say has the type of F and the parameters foo have the types t1, t2 all the way up to tk. So we know that F has the type t1, t2, F is a function, so F has this node has the type of F which is a function that takes in k parameters and those k parameters have the types t1, t2 all the way up to tk that return a type r. And so this is how function application ties those constraints together. So what about a function definition? So we can define a function called foo that takes in parameters x1, x2, all the way to xk and that let's say is equal to some expression. And remember this expression now can be arbitrarily complex tree. So here we can define a function foo. So our definition is gonna be the parent here. Now in this case the leftmost child is going to be this function definition. So here we have foo x1, x2 all the way up to xk and so they have types foo has the type F, x1 has the type t1, x2 has the type t2 all the way up to xk is the type xk and now we have an expression which has the type e. So once again from this what are the constraints that we can infer about the function, the program of the types here? So what we know is we know that F is a function type. So we know that definitely. And we know that that function type has k parameters and we know those k parameters have the types t1, t2 all the way up to tk. And we know that foo is a function that returns the type e. So whatever this expression, the type of whatever this expression returns, well that's gonna be the return type of foo. So we know that foo has the type F and that type F is t1, t2 all the way up to tk and returns a type e. Okay, one more thing we need to look at is if expressions. So once again I'll reiterate this is not a complete list of functions, of semantics of the constraints here, but you can see that we're defining it for kind of a subset of the language and this is what you'll use for your homework assignment. So what we have in an if expression, right? So the general form of an if expression is if condition then expression one, otherwise expression two. So here we're gonna have four nodes. Our parent node is gonna be the if statement and remember the if statement, the if is going to return something. So that return type will be t4. And then the children are gonna be, the leftmost children is gonna be the condition. So here we're gonna have the condition and we're gonna give it type t1. Then we're gonna have an expression one with type t2 and expression two with type t3. So what do we know about the types here from the semantics of an if statement? Well, we know that we saw earlier in the example. So t1 here is a condition, it's the if condition, so its type must be a Boolean. Then we also know that remember our if condition is returning something, so it better be the case that no matter which branch we go down, the return type better be the same. Otherwise the types are messed up and won't work. So we know that the type of t3 must be equal to the type of t2, excuse me, which is the type of expression one, must be equal to the type t3, which is the type of expression two. And both of those must be equal to the type of t4. So this is simply saying that t4, whatever the if statement returns, the type of that has to be the type of whatever's in the then branch and whatever's in the else branch. Okay, so now we've defined all of the constraints in our language, now we need to unify do what's called type unification. And this is essentially the process by which constraints are propagated. So you can think of it kind of like we start with these constraints as a base case and we initially give everything arbitrary types and then these constraints constrain what those types can be so that we have, since we started with a very general type, these constraints will give us something that is valid for the usage of the program that type checks but is the most general types that we can give. So the basic idea is very simple. So you first build this tree from the descriptions and these will be given to you in all the examples. So you first build the tree and you start from the top of the tree. So every time you see a construct and each time you see a construct that has an unconstrained type, you create a new type. So if you see something that you don't have a type for, well you just give it a new type. But, and this is the key. Excuse me. If a construct is found to have type T1 and also to have type T2, then T1 and T2 must be the same type for the type system, for the types to be valid. So what this means is if you're going through checking the types and you see that, this node has to have a type of an integer and but earlier you gave it the type T1, well that means T1 must be an integer. And same with how you can check for errors. Remember in our example, we saw a construct, it was a variable A that had the type int and then later we saw, well actually A has the type of function and function can't be equal to an integer and they can't be made the same. They can't be unified to be the same so therefore we can get rid of it. Okay, so let's walk through an example of how to do this type unification and how to apply Hindley Milner type inference to type unification. So let's use our example of function foo takes in parameters A, B and C and returns C applied to A bracket B. So first thing we do is we're gonna build our tree. So we first have at the top level right here, we're just looking at the function definition. So at the top level at the root of our tree is going to be a define node. And remember the leftmost child of a define node is going to be the function so it's gonna be foo A, B, C and the rightmost is going to be whatever that expression is. So what's the expression on the right hand side? Well, it's a function application, right? So we're calling the function C and passing in whatever's inside the parentheses. So here you have the function apply and if you look you'll notice that I'm numbering the nodes and we'll see why in a second. But I can just build this tree, I don't need the numbers, right? So apply, remember our semantics for the tree for apply has the leftmost node, the leftmost child to be the function that's called. So here we have the function C or we have the variable C and on the right hand side we have actually a array access operator. So here an array access operator has the leftmost child as the array here in the expression A and the rightmost operator, a rightmost child to be whatever's in the index which is B. So what I'm going to do is first I'm going, so I numbered, you can see I numbered the nodes basically in a pre-order numbering. One, two, three, four, five, six. Our pre-order traversal. I didn't number this node here, it should be, it'll be clear in a second but this is kind of a, let's say a helper node, but we're numbering it in the order that we're going to visit those, visit each of the nodes. And so in order to calculate this I'm going to make a table of, well every, I'm going to make a table of every name and variable in the program and also each of the nodes. So remember here I have the nodes one, two, three, four, five, six. So the types of those nodes are going to be specified based on the constraints that we just talked about. So here I have my table. So I have foo, ABC, one, two, three, four, five, six. Okay. So I started the top at the root node at node one. And at node one I say, okay, what constraints do I know about node one? Well node one is a function definition. So what I first know is that I have three parameters, A, B, and C, and they each have their own distinct types. There's, I know that there's no constraints on those types based on a function definition. So I know A, B, and C have types one, two, and three. And you'll notice this table kind of grows in order to expand. Okay. Furthermore, I know that foo is a function and that function takes in three parameters and returns something. And those three parameters have types T1, T2, and T3 and it returns something but we don't know what it returns. But we have no constraints that it's exact same type as A, B, or C. So we're gonna create a new type T4. Okay. Well, not much constraints are happening at this moment. But we do know something very important in that we know that two, right, the right child of a define of a function definition, the right child, that's return, that's type has to be the same as the return type of the function foo. So I know, I already know now the return type of two and that means it's gotta be a T4, right? So whatever the function foo returns, well, it's gonna return whatever node twos type is. Okay. So one question, why didn't I assign a type to one? Well, I kind of just added in here. It really depends on the language semantics if you're gonna return something for a function definition. And so here we're gonna say no, we're not returning anything here. But it's possible to return a type here. So you'd actually want to know the type of a function definition. But here we're gonna say we don't really care about that. Okay. So now we're gonna look at type two, node two. Node two. We're gonna say, okay, what are the constraints here based on the semantics that we just looked at? So here we have a function application. So what am I applying? So here I have a function application and I know that node three is gonna be some function and I know that what's on the right is gonna be a parameter of that function. So I know that whatever's on the right is going to be arbitrary, something five. Let's say T five, we have no constraints on whatever that type is. Because I know when I have a function application, I don't have any constraints on what the types of the function parameters are. Okay. But the type of T three, I know the type of T three based on the information I have now. I know that the type of three of node three is something that takes in a T five. So it takes in whatever this type is in T four and it returns what? Well, it returns, not something arbitrary, it returns whatever is the type of node two. So I know that node three is a function. I know it's a function because of the semantics of the supply. And I know that node three is a function that takes in a T five and returns a T four. And remember this T four is the same as the type of node two and it's the same as the return type of function foo. And this makes sense just intuitively if you think about it, right? So foo is gonna return whatever C returns. So therefore the return type of foo better be the same as the return type of C. Okay. Now one thing to notice, well, what we did was just apply our type inference just to node two and the constraints there. So you may see, well, we have C here as the type T three and that's because we haven't looked at node three yet. So when we look at node three, we see, oh, well node three is just C. So that means that C, which we know as the type T three has to have the same type as node three, which is the function that takes in T five and returns T four. So this is where we do type unification. Remember here, we've got to the point where we see, hmm, I know these two types have to be equivalent. T three has to be equivalent to the type of C has to be equivalent to the type of three. So what I'm gonna do is I'm going to get rid of T three by replacing it everywhere with T five returns T four. So I'm gonna first replace it here in this. So I know C now has the type C as a function that takes in type T five and returns T four. But I'm also gonna change this T three that's in foo and say, well, that's actually now a T five goes to T four. So you can see I've used the constraints already so far in the program in order to eliminate this type T three and say, hey, C actually can't be an arbitrary type. It has to have a specific, it has to be a function. And right now all I know is it has to take in some type T five and return some type T four. Okay, so I visited node three. Now I'm gonna visit node four. And when I visit node four, now I'm gonna say, okay, well, node four is type T five, right? We know that right now. It's gotta be type T five. But I know from the semantics of the constraints of the array operator that the type of, I know that something about the types of the left child and the right child. So I know that the type of five has to be an array of what? It has to be an array because it's being used by the bracket operator. It's got to be an array, but what's the type? So we know what the, we know it has to be an array of, well, whatever four returns, right? So we can actually say that node five is an array of T fives. And we know that because four has to return a T five. But we also know something else, right? We also know that node six has to have the type of integer because of the constraints of the array operator. So we know that node six is a type integer. So now I'm done visiting node four. I'm going to visit node five. And now this is when I say, hmm, a is the same type as node five, which means that you need to unify those types. And I need to unify T one with array of T five. So what I'm going to do is I'm going to replace every T one with array of T five. So I'm going to replace here this A and I'm also going to replace the signature of foo here. Okay, awesome. So now that I visited node five, now I'm going to go and I'm going to visit node six. And I'm going to say, okay, well six has type int and I see it's maybe getting a little cutoff on my version, I'm not sure about what you're saying. But I know six is a type integer. And but I also know that six is just the variable B. I know B has the type T two. So to unify that, that means T two has to be an integer. So I know that I can replace this T two with an int and then I know I can look at this T two here in foo and replace that with an int. And now I've gone through all the nodes. So in the cases we'll, you know, I don't want to, we don't want to get too complicated because we're just going to look at how you can apply this to function definitions and not whole programs. If it was whole programs, you'd continue iterating over this, doing this until nothing has changed. But here we can say that, well, the type of foo. So now we know we've gone, visited every node, we applied all of our constraints and we did our type unification. So now that we know that this function foo takes in three parameters, the first parameter is array of some type T five, the second parameter is an integer and the third parameter is a function that takes in the same type of whatever that array is and returns a new type T four and that that returns a T four. So by using this, with Hindley-Milner type inference, we're able to completely and statically infer the types, the most general types for all the types in our program. And so it's very cool, very cool technique. You'll be using this on your next homework assignment and on the next project. So yeah, if you have any questions, feel free to email the mailing list or go to the discussion that will be on Wednesday during normal class time. Thank you very much. Talk to you again later. See you next Monday.