[MUSIC] Now that we've actually learned a fair amount of both Racket programming and ML programming. I feel we are in a good place to finally compare and contract the two languages and their relative strengths and weaknesses. So there are a lot of ways to do this. And we should first point out that Racket and ML have a lot in common. They both have high order functions. They both discourage mutation. They both have eager evaluation of function arguments. There's a lot that really are the same. Of course it's more interesting to focus on the differences. And the first one you might think of is the syntax. The regular syntax and parenthesis based syntax of Racket versus the more sophisticated syntax of ML. The fact that ML's pattern matching where versus the kind of structs that we saw in Racket. The different semantics for the various kinds of let-expressions is something that Racket has and ML doesn't and so on. But I think it's fair to say the biggest difference in approach of the two languages is that ML has a sophisticated type system that rejects a lot of programs before they ever start to run. And Racket chooses not to, to be more permissive in its set of programs and, instead, have a wider class of errors that happened while the program is evaluating. And it's that distinction that we're going to really focus on for pretty much the rest of this section. So we have a lot of questions coming up. We're going to ask questions like, what exactly is type-checking? What does it mean to check for something statically before a program runs? Why a type checker has to be approximate in terms of what it's trying to accomplish. And then we'll build to the real question that people love to argue about which is whether your language should do static checking, static type checking or not. But before we get to that, I think it'll be helpful to better appreciate ML and Racket as languages themselves. In terms of how a Racket programmer might think about ML's type system. And then how an ML programmer might think about Racket's lack of static type checking, and how you might code up the Racket style in ML. So that's what we're going to do here, I'll give you a nice context before we get into these more precise questions in the upcoming segment. So first let's imagine you're a Racket programmer, that's the language you know and love, and then you learn ML. And let's ignore the syntax and the different kind of let expressions and the module system and all that and just focus on this issue of the type system. Probably the most natural thing for a Racket programmer to think is that all ML is not just Racket, but it is a subset of Racket. It takes all the Racket programs, the type system gets rid of a lot of them, and then you have ML. And that's a perspective I want to share here on this slide. And you might be very happy because a lot of the programs that ML gets rid of have bugs on them. You wouldn't want to write them, and so it's nice that it catches those errors and throws them away from you. So you'll have, say, a function like this function g, that takes an x and adds x to x. There's no bug there, it type checks. The fact that it's int arrow int is nice and all, but the point is, it is in the language. Whereas these functions f and h, if you translate them to the corresponding syntax in ML, would not type check. And I would argue that's a good thing. This function f even though we don't see any call to it in the part of the program we're looking at here, you can tell its bug. Any call to f is going to raise an error because we pass y the argument both the plus and to car. And there's simply no value in Racket that that's going to work for, we're always going to get an error so it's nice to know that without having to test f. And then here in h, it actually looks perfectly reasonable taken at argument z, cons z 2, called g with the result. But if you actually look at what g is bound to in our environment up here in this first line, we can see that will raise an error as well. We know this, it's nice to catch these bugs. In fact the subset is well enough to find that we do know the type of everything everywhere. So for example, in the programs that are left in our subset view of the world, you never have to use built in primitives like is something a number. You always know statically, and there'd be no reason to at run time have to evaluate a question like is this value a number? And that's pretty neat. On the other hand, you might not be so happy with this subset. Because the subset also gets rid of programs that you're used to writing in Racket and that don't have bugs. So this function f here happens to have an if expression that sometimes returns a boolean and sometimes returns a list. There's nothing wrong with that in Racket, it cannot type check in ML. Similarly, this xs builds a list holding different kinds of data. Can't do that directly either. And then, on this last line it's just a call to f using xs that all type checks, all works fine, and a Racket programmer would look at this and say, okay, I'm going to get a true back. And there's nothing wrong with this, but ML doesn't allow it. Okay, so that's ML from a Racket perspective. You might think that Racket from an ML perspective would just be the opposite, that it's just a superset. And that is one perspective but I want to show you a more interesting one. There are ML programmers out there who like to think of Racket very differently, they like to think of Racket as just a particular style of ML programming, a very strange style. But it turns out every Racket program can be thought of an LML program, or almost. Show you on the next slide that this doesn't quite work. The way to think about this, is everything in Racket is really just part of one big data type. It's not that Racket doesn't have types, it's that everything has the same type. Let's call it this data type, theType, the one type. So every value is either the constructor int applied to some int, or the constructor string applied to some string, or the constructor pair for con cells apply to two values of the type. It's a recursive data type binding. Functions just take in something of theType, and return theType. I guess since we have multi-argument functions, we can say it takes a list of the theTypes. And you would have one of these constructors for every built in kind of thing in the language. And then from this very ML view of the world, what Racket expressions evaluate to are values of theType because the Racket interpreter, the Racket implementation adds the constructors for you automatically. So you just write 42, but it's like you wrote the constructor Int applied to 42, and Racket does this for you, so every value passed along has value theType so there's always a tag that tells you what type everything is. And we use that tag so that functions like car can raise errors. And functions like pair question mark can see whether they should return true or false. So in this world where everything is tagged, we can think of functions like car and pair question mark, as doing the ML pattern matching under the implementation where you can't see it. So car just takes in some value, cases on it, if it's made from the pair constructor return the first thing otherwise raise an exception, cause some error. Similarly pair question mark is the same sort of pattern matching and uses it to return true or false. So everything works this way. Plus just takes in two arguments, pattern matches to get out the underlying number, raises an exception if it's not given a number. Adds those two things together and then tags the result with int so that everyone else in our program can continue to have those tags. So really everything in Racket is just using theType which you see defined here. So, all you need to do this is to have a built in constructor for every primitive kind of data in your program. You need numbers and strings and booleans and pairs and symbols and procedures and so on. The only thing this does not cover is structs. So when an ML programmer looks at the struct in Racket, they see something that is actually new different. Because if you have this one type for everything view of the world, what a struct definition is doing is dynamically, while the program is executing, adding a new kind of constructor to theType. And you can do that, it's well defined. You wouldn't want to do it in ML because it would mess up things like checking exhaustiveness in pattern matching. But it does makes sense that you could have a data type binding that you can add new constructors to, ML does not allow it, but we can think of it as a sensible thing. In fact in ML, this how the exception type EXN works. But that's kind of an obscure feature that is only sometimes used in that way. More generally, I just like this perspective. It's kind of a mind bending way to think of Racket as a programming language that doesn't have no types. It has one type. That's a very ML-centric way of looking at Racket.