 Thank you for joining us. We have Tony Morris here. He's here to talk about types and property tests for software residency. And Tony, for those of you who don't know, is coming to us from Australia. He is a flight instructor, pilot and UKG slingsome has to code as well. He does a lot of other things, but I think I'm going to leave Tony to talk about everything and take it away Tony. All righty. Thanks for that introduction. Hello everybody. So as was mentioned, I live in Brisbane, Australia on the east coast of Australia. It's dark outside right now. I'm missing India, that's for sure. I've been there a couple of times. I love it every time I go there, but unfortunately I couldn't make it this year. So hopefully next year I'll be able to come and visit you guys. So what I'm going to be talking about today or tonight, it's tonight here, is types and property tests. And my goal is just to introduce the idea and give it just a basic introduction. I'm watching the, I think it's called discuss or chat or something, yeah discuss. I'm just watching that. If you have any questions, please feel free to type those in. And if it's quick and easy to answer the question, I will. And if not, I'll address it at the end. So let's get started. I can't really see my audience. I hope you are ready for an introductory level talk in terms of just a different way to determine whether or not your software is working as you desire it to be working. And some of the tools that I use in my software engineering role to make those decisions. So let's get started. By the way, I work for a company called Simple Machines. They're based in Sydney, which is, you know, that way south of Brisbane. And it's a great, great team that I work on with a bunch of really skilled programmers. And, you know, we do functional programming. We do not functional programming. We do all kinds of programming around, usually around data processing or something similar. So that's Simple Machines. So a little bit about, it's a question that I get quite often, which is, you know, why do you care about functional programming? Why? How did you get into it? And so on. So I'll just give you a bit of an introduction to that question, which is, you know, when 20 years ago or so, I was, I used to work at IBM and I was working on the JDK for IBM. So IBM produces a JDK. And I was also working on the IBM WebSphere application server. So I was writing a lot of Java. That's what I was doing all day long, every day, writing Java, writing Java and so on. And if you ever tried to write Java, you'll probably run into all the things that happen, the null pointer exception, scrolling past your screen and all that sort of stuff. And I simply just thought to myself, surely there's a better way to do software engineering than this. You know, I've only been out of university for a couple of years. And whenever I think there's a better way to do something, my next immediate thought is someone smarter than me has figured out how to do it. What is it? Where should I look? So that principle has worked well for me in my career, just find the smarter people and figure out what they've been doing. They've figured it out. The problem with that is, of course, you've got to, it's not always as easy as just saying who is the smart people. That's everybody, right? So it's quite a battle to figure that out. But so I was searching for quite a while, and I finally found out that there is, in fact, a better way to do software engineering than I was doing at the time. It even has a name, and it's called functional programming. And I didn't know that 20 years ago, or at least that's when I learned it 20 years ago. That's how you do software engineering without all of these problems that I kept running into when I was working. I actually used to work in L3 support on the JDK itself. And if you've ever worked on a support role for large corporate software, you might be able to sympathize with a bit of the pain that I was going through and some of the questions that I asked of myself and my career at the time as well. There's got to be a better way to do software engineering than what I was doing. Turns out that there is, it's called functional programming. So that's what I learned 20 years ago. I've not yet found a reason for that to be incorrect. I still believe it to be true. So one problem, and especially recently in recent years, the meaning of functional programming is not always immediately available if you really wanted to know, if you wanted to sort of dive in the hole yourself and figure it out. It has a very concrete meaning. It's very trivial. It doesn't take long to explain what it means. And also to understand what it means. But there are very far reaching consequences. So what I'm going to do is I'm going to tell you what functional programming actually means. And then we'll look at a few of the consequences and one of those has to do with types of tests. So let's talk about what it means. So what I've got here is just a computer program. It's written in, it doesn't matter what programming language. Hopefully you can read that syntax. And what that little wiggle procedure does there is it updates some counters off somewhere and then it does something with two integers and returns a value. But importantly, that arbitrary code there, that could stand for a million lines of code or a billion doesn't matter. It's just any amount of code. And then you come across just one line of code there that uses the wiggle procedure. And if you have a look at that one line of code, you'll see that there is a repeated expression within that little procedure there. So that is to say, with the arguments as well, that expression is repeated exactly. And so if you come across such a repetition of expressions, you might ask, well, you know what, I'm going to factor out that expression and I'm going to assign it a value. So in this case, it's been assigned the value R. And then we've replaced that expression with the value. And there's a question that we can now ask about this program. And the question that we might ask is, did the program just change? So I'll just go back again just so you can see that. So when I factor out this, sorry, when I factor out this expression, did the program just change? So the answer to that question is, yes, it did. It did change. And it changed because of this counter equals counter plus one. It used to update the counter only once and now, sorry, twice and now it only updates once. And so the counter will have a different value after this change to the program. And when I say, and just to clarify what I mean when I say a program changes, that is to say it's inputs and its outputs have changed. So for its given inputs, so if I ran this program with some inputs, I should see a result. And then if I change the program and give it the same inputs, I should see the same result. But in this case, I will see a different result because the program has changed. And so let's look at a different example now. And what this little function does, called Pipple, is it takes in two integers and it adds them together and multiplies them by two and then returns that value. And again, there's, you know, it could be a thousand, could be a million lines of code or whatever. And then you come across just this one line of code and you see a repeated expression again. So there's the repeated expression. And you might ask the question, well, can I factor that out? I'm going to assign it a value. So you do and you factor it out and you assign it the value r. And we asked the same question. This time, did the program just change? Now, in this case, it's the same program. So I'll just go back just so that you can have a look. If I were to, I don't know, you can see my cursor yet. So if I were to factor out this expression here and assign it, assign it a value, then and then replace that expression with the value, the program remains unchanged. It's the same program. So for the given inputs, the same outputs. So it's the same program. Now, what is functional programming, right? So you've seen an example where changing that expression alters the program. And you've seen an example where it doesn't change the program. Functional programming is the idea that we can always do that. Okay, so for any given sub expression in a program, we can always replace expressions with a value without affecting the program. That is simply the meaning for functional programming. But this has some pretty far reaching consequences. So one of those consequences is, for example, if you wanted to add up the numbers in a list, all right? So I want you to imagine yourself and you've sort of put this stake in the ground and you basically said, you know what, I am going to always adhere to this principle. I will never break it. I will do functional programming. But the problem is, is some of your tools for doing even simple things like adding up the numbers in a list are no longer available. Consider, for example, this little bit of code here. So this is how you might consider adding up the numbers in a list. So you set the value r to zero, you look through the list, you add each list element to the variable r, and then you return r. And that will add up all the numbers in the list. I agree that it does. However, notice that you've violated the principle. You are not functional programming anymore. Because if I can replace expressions with values at all times, then I could, for example, replace r with zero. Oops, sorry, I keep clicking accidentally. If I replace r with zero throughout this entire program, then this little procedure here will always return zero. And so that will change the program. Obviously, it will always return zero. So that's, this is not how you sum the numbers in a list once you've committed to the principle of functional programming. But there's another way to look at this problem, that the problem being summing the numbers in a list, how to add them up. So let's look at that other way, just another way of thinking about the problem of adding up the numbers in a list. And I want to encourage this idea of just looking at the problem in a completely different way. And if you were to do that, if you were to look at problems in different ways altogether, then I think it would help in introducing yourself to functional programming and the different tools that it has to solve problems. So I'm just going to put it to you that this is the definition of the summing of a list, which is if the list is empty, then it's zero. Otherwise, take the first element and add it to the sum of the rest of the list. That's a pretty straightforward definition for adding up the numbers in a list. But you'll see that it's a recursive definition. And by that I mean, we're trying to define this term here, this term being the sum of a list. And in the definition, we've had to reference summing a list. So in defining the sum of a list, we've talked about summing a list. So we have what's called a recursive definition here for adding up the numbers in a list, which, if you've not done much functional programming before, it can be an unfamiliar way to look at problem solving. So all I really have to recommend there is just practice and practice. And it does become quite natural and easy. In the same way that the loop, if you can remember back to the days that you learned how a loop works, and you would have clumsily tried to figure out how does this loop work and so on. And eventually you would have found it quite easy and natural. And the same is true for recursive definitions. You just need to practice and eventually it all starts to make sense. So consider, for example, this list here, the list being 65971 and 3. The definition of the sum of this list is 6 plus the sum of the rest of the list, which is equal to and so on. We keep calling sum all the way down until we get the sum of the empty list, which is 0. And we'll end up with 94. So no loops were involved in solving this problem. And we didn't violate the principle of functional programming. And if you're not sure that we violated the principle of functional programming, here's the actual source code written in the programming language called Haskell, which I'm going to be using throughout this presentation. I'll use a bit of Haskell and a bit of Java. I hope that's okay. But this is the actual source code. And it basically says the sum of the empty list here is 0. And the sum of the first element followed by the rest is the first element plus the sum of the rest. And you can see that recursion going on here. On the left side of equals is the definition. And on the right side of equals is the solution. But the solution appears on both the left and the right hand side of the equal sign. And therefore it's recursive. Okay. So that's just functional programming. And like I said, I should be able to replace all the expressions with their value in this code and I can. And in fact, that's true of all Haskell source code. Haskell enforces the principle. And it won't allow you to violate it, even if you try what you want to. So there are other consequences. Not just, you know, just trivial examples of different problem solving methods. But what the one that I'm hoping to talk about today is the ability to efficiently and effectively ensure our software is correct. Now, I'm not using these terms lightly. So I wanted to I want to increase, at least increase my confidence that my software is correct. So I want to be effective. And I want to do it quickly. I want to be able to not take a long time to do it. I want to be able to do it. I just want to write a short amount of code and have a reliable result in my software being correct. I'll give you a contrast. Now, who has one of these software systems and I'm not really saying, you know, put your hand up. I have seen these software systems. Maybe you've got one at your office through the week. Now, usually what happens is the tests take a few hours to run. All right. You know, an hour, maybe two, something like that. And so you commit and push and go to the coffee shop and hope. But, you know, you distract yourself with the coffee. And on your way back, you know, you get an email that comes on your phone and it says, build failed. And you're like, damn it. Well, because of some silly reason, it couldn't find the comp file or whatever the silly reason is. And so you fix the build and then the test finally they passed. But that took you all day. And now you're on your third coffee. And so you put it into production, right? The test pass. Let's do it. And it falls over anyway. You know, you see an exception or, you know, the customer rings you up or whatever it might be. It didn't really give you an assurance that your software is working. But it's working on your machine. And so what you do now is you just go back to step one on the next day, go to the coffee shop and so on. And I would call this a very inefficient and very ineffective system of testing that your software works. It's inefficient because it takes two hours to run. And, you know, you have to distract yourself with the coffee. And it's ineffective because the software doesn't work anyway, even when the test pass. So I just want to make it clear that because we are functional programming, we can use the types to determine the behavior of our software. And we can use this idea of automated testing, which I'm going to talk about to make up for where types have left gaps. So all the examples that I show you from here on, I just want you to remember that we are functional programming. And that's why I can make the claims that I'm going to make about some of these examples. So there's another sort of little footnote to this. Some programming languages have escape hatches from the types. For example, null or throwing exceptions or casting types, just saying, you know, this type A, you just make it into a B, or type casing and just say, well, is this value of this type? Does it have this type? Or does it have some other type? And also non-termination, which is basically a function that just runs forever. So these are what we'll call escape hatches. And there's a paper called Fast and Loose Reasoning is Morally Correct. That's the name of the paper. And this is sort of the introduction to the paper. And it says, functional programmers reason about programmers as if they were written in a total language. Now, a total language is one that does not have those escape hatches. They simply don't exist in those languages. But they, and then expecting the results to carry over to non-total languages. So for example, Java Haskell is a non-total language. And so what this paper does is it says, when you reason about your programs, you can ignore these escape hatches, right? So things like this, they simply don't exist. Now, of course, they do exist. So what this paper is all about is reasoning about our programs as if they didn't exist. So from here on in, we've got two states in the ground. One of them is we are functional programming. And the other one is we are reasoning as if we're in a total language. So we can ignore the escape hatches. Okay, so consider this program here is written in, I guess, Java. It looks like Java. It takes a Boolean and it returns a Boolean. And I'm pretty keen if anyone has an answer to this question to hear from you, how many possible programs can be written that satisfy this type? And by that I mean, how many different things can be written here so that the type is satisfied? And remember that program equivalence, so two programs are equal, if given an input, we get the same, given a set of inputs, we get the same output. So by that, I mean, you know, if we write a loop that counts up to a thousand and then return a value, that's the same as if it didn't do the loop at all, because it's not an observable difference. So how many possible programs exist that have this type, assuming we are functional programming and assuming we are doing fast and loose reasoning. So it's going to terminate, it's not going to use an escape hatch. Does anyone know the answer? I'm going to have a quick sit while I wait for answers. Oops. Yeah, I'm watching the discussions. We've had one guess of two. It's a good guess. Any other guesses or only guesses are fine. It's okay to, you know, it's okay if you don't get the right answer. That's okay. Any other guesses? Or not guesses? Any other answers? Come on. Don't be shy. I'll tell you, I'll just put an answer in. Here you go. I'm going to say 99. 99 is not correct. All right. Note four to five. Somewhere, so four and a half. Four to five. Okay. We're going to look at the answer to that question a bit later on. Well, I'll tell you what, I'll tell you what we'll do is I'll tell you the answer and then I'll tell you how to calculate the answer. So the answer in this case is four. There are four possible programs with that type. And if you are unsure, there's actually a way to calculate it. And it's quite quick and easy to do. So later on, I'm going to try and show you how to calculate it. So the answer is four. If you've got a pen and paper in front of you, maybe you can try and try and figure them all out. You'll end up four. And I assure you, you're not going to be able to find a fifth one. If you do, it'll just be the same as one of the other four. But if we have a look at this sort of type here, it takes a string and returns a string. There's quite a large number of programs with that type. So it's not four in this case. The answer is a large. I haven't calculated. It's very, very large. It depends on the size of string, which is in the JVN specification somewhere, the maximum size of a string. And I've forgotten what it is. But let's have a look at this type here. Now, does anyone have any idea or any suggestions about how many values have this type? How many programs that I possibly write that takes generic A and returns a generic A? How many possible programs exist? I'm really keen to hear any answers to this one. Any suggestions? So remember, we are functional programming and we must terminate. We can't use an escape hatch. The answer in this case is one. There is only one program with this type. And you can probably work out what it is. It's simply returned this argument here. And there are no other programs with that type. Or in other words, the type told us the program. Given that type, we can actually work out the program. And we did that because we used what's called a polymorphic value or a generic. Because we used a polymorphic value, it wasn't given a Boolean return a Boolean. It wasn't given a string return a string. We used a polymorphic value. And so by doing so, we've restricted the possible functions with that type to be one. And I've actually written a proof of that. And it's later on in these slides. There's a link in these slides to that proof that it is equal to one. But we have actually obtained total knowledge of the function just given the type. And the idea of using types to work that out is called parametricity. If you type that into a search engine or something like that, if you wanted to look it up, I'm using parametricity to determine that there's only one function with that type. And the paper, if you want to read all about it, it's called our theorems for free by Philip Wadler. And basically what this paper says is write down the definition of a polymorphic function. So that is just anything using generics. And tell me it's type, but be careful not to let me see the definition of the function. And Phil will tell you a theorem that the function satisfied. And the paper explains how to do that. It's quite simple to do with a little bit of practice. So I'll give you an example. So what this is referring to is something called free theorems or theorems for free. So I'll give you an example of a free theorem. If we have a look at this type, there is no polymorphism in this type. And if we said what is a free theorem about this type, and there's really nothing I could tell you, other than it's not a function that takes, it doesn't like add 10 to its argument because it's not an integer argument. It's a list of string and it returns a list of strings. But we already know that by looking at the type. We don't gain any extra information by looking at the type because it's not polymorphic. So it's, yeah, it's just simply not polymorphic. But let's have a look at this type. And I want you to convince yourself that my theorem that I've constructed here is a true statement. There's nothing any programmer can do and can possibly write as a function with that type that violates my theorem. My theorem holds no matter what is written within that function body. And I can determine that theorem by looking only at the type. So I looked at the type and I said, well, it takes a generic list of t and returns a generic list of t. And immediately I can conclude that every element in the result appears in the input list. So this is an example of a free theorem. And I can calculate that free theorem. But just intuitively, I just want you to feel it and just say, well, yeah, that must be true. If you disagree, you know, feel free to write a question. That's okay. So any every function that is of that type must satisfy that theorem. So that's an example of a free theorem. So we but the thing is, is we have some information about this, this function, but we don't have total information. We don't know exactly what it does. All we know is that the theorem satisfies. So what I'm going to do now is because I've used types up to a point that there's a gap and the gap is it's if we still don't know that completely what the function does just by looking at the type, I'm going to write what's called an automated test. And what and so if we have a look at so this is an example of a test that's written in Haskell, that's really only because it fits on the slide. You can write this in Java. But I'll just talk you through what this code is doing. And this is called a property or a property test. And what it says is, for all characters, alpha characters, call those X, then if I put that X, so just one character, if I put that into an into a list, so a single element list, and then I call my function, then it just returns the same list. So given a single element list, it remains unchanged if I call this function. This still doesn't tell me everything about the function. It tells me a little bit more. But knowing that this test passes is not enough to determine what that function does in its entirety. So I'm going to write yet another test. So here's the second test. And what this test does is it generates two lists X and Y. So given two lists of characters X and Y. And what this says is if I if I append X and Y, so I just join them together X and Y, and then I call my function, then this is the same as calling my function with Y and calling my function with X and then appending those two things. Does anyone know what this function does? Anyone? Because that is total information. I now know what that function does. So if anyone has any idea about what that function does, I'll be keen to hear from you. I'll give you an example of what it doesn't do. It doesn't simply return the X. All right. If it just returned X, then this test would fail. You can see here that append X and Y. This is for all lists X and Y. I call my function and then I call my function on Y, call my function on X and append that. If this function simply returned X, then this test would fail. And I want you to sort of think about how this test is running. It's generating hundreds and thousands of lists X and Y and making sure that this test passes. All right. So it doesn't just return the argument. And this idea is called property-based testing. Someone has just proposed that this reverses the list. And that is the correct answer. It does reverse the list and it must reverse the list. And that's the only function that is going to pass these tests. No other function will pass the tests. And this is an example of now we have total information about the function. It must reverse the list. So yeah, good on you. It does reverse the list. So let's talk about what we just did there. So just going back to this test, actually, this test is not exhaustive. It does not punch in every single possible list. It just puts in thousands of lists by default across a random distribution. But you can configure it if you wanted to change it. The way that it distributes the generation of lists. But it doesn't exhaust all of it. So if we just talk about what we just did, types provide what's called a proof of a proposition. So for example, the argument is a list of polymorphic tea. It's a very trivial proposition like we kind of already knew that. That's what the types are doing. They're providing a proof. Polymorphic types provide additional theorems. So for example, we've got a free theorem about that type because it was polymorphic. And what tests do is they they they attempt to negatively prove the proposition, and then hopefully fail. That means that the test pass. And that is actually the only difference between types and tests. And I just wanted to emphasize that because I've heard lately, you know, commentary that types and tests have nothing to do with each other. That's completely incorrect. Types and tests are incredibly closely related. The only difference being the kind of information they're providing. So types are providing a proof and tests are providing a failed negative proof. That's actually the only difference between types and tests. So this type here, like I said earlier, has only one possible answer. And it's an example of once inhabitants. That is to say, there's only one program with this type. And there's no tests that we could possibly write for this type, because they're all going to be redundant. We've proven it all the way to totality. We know what the function does. It must return its argument. That's all it can do. The types give us this information. And it's a really good exercise. If you're a little bit like, oh, I'm not too sure if that's true, it's a really good exercise to try to make it not true. Like to sit down with a compiler and see and try to make it not true. You'll either use an escape hatch, or you'll not terminate, or you won't be functional programming. You'll start doing something where I can't replace expressions with values anymore. So because we've got these states in the ground, you're only ever going to come up with one answer. Let's talk about some more realistic examples. These are sort of just definitions and trivial examples. But let's look at this one here. It's still a little bit trivial, but a little bit less trivial. If we have a look at, say, this validate web form function here, and then this other function here. This is written in Haskell notation. So I'll just read it out to you. Submit web form, takes as arguments an app state, and then a web form, and then it returns back a pair of a response and an app state. So it's just an example I came up with. That's typically what a web form does sitting on a server. We submit the web form with the state of the application sitting on the server, and then the web form itself. We get back a new state, and then a response to send back to the browser. I'm going to make the claim that for all web forms and for all app states, if I submit the web form twice, so if I submit the web form with a state, and then I submit it again with the same state, then that's the same as submitting it just once. And you can see here, it runs the two actions, calls the results S1 and S2, and then makes sure that they're equivalent in the test property. This little property here is called like dampenance. It actually has a name. If we do it twice, it's the same as doing it once. So this is an example of a property test I might write if I'm writing a web application, and the test framework is going to generate thousands and thousands of different web forms and application states. It's going to try to disprove this property, and if it doesn't, it's going to say it passed. It's not going to provide me a proof because that's not what tests do. That's what types do, but it is going to try really hard to disprove it and hopefully fail. That means that the test passed. So just as a bit of like a principle in how to use this sort of method of software engineering, we use the types first, and we use them up to the point that we can, and where they fall short, we start using automated tests. The library that I use, this is written in Haskell, and it's written in Scala as well, is called Hedgehog. So if you've got a bunch of Java codes sitting at your work or whatever, you can actually use Hedgehog written in Scala to write some tests and call into those Java libraries, and you can generate automated tests. The goal that I really hope for is not that it works on my machine right now, but it works on all machines at all times. That is to say my tests are deterministic. Just because they passed on Tuesday, that also means they'll pass on Wednesday, and I'm sure you've all seen that in certain production systems where you run the tests and then someone updates a variable somewhere and suddenly the tests stop working. So back to this example, like I said, the answer is four, and you can actually calculate it algebraically. So you can do this for any function. The inhabitants or the number of programs with the type is the return type. So that is to say Boolean being the return type, raised to the power of its argument. So Boolean to the power of Boolean, or two to the power of two, and that's four. And if you're not sure, here are all the possible programs with that type. That's all four, and there's nothing else you can do. That's the only four possible programs you can write with that type. What about this one? So given a polymorphic A return and A, I've told you that it's only one, but how do I work out that it's actually equal to one? Now I wrote a proof, and I wrote that proof using Java, all right? Now, it turns out that it was about 300 lines of code, which of course is not going to fit on my slides. But I did write this proof. I wrote it using the Yoneta lemma, and I wrote it in Java because, you know, I like to push certain programming languages to their boundaries and see what they can do. For example, can I write a proof that this is equal to one using the Yoneta lemma? The answer is yes, I did. And it's available there. And I'll post the link to this later. And actually, I can probably, maybe I can do that right now. I'll post it. Can I do that right now? I'll post it in the chat. So there's the proof there. That is equal to one using a NetElima. And don't blame me for its messiness. Like I said, I chose Java for a reason just because I just wanted to see if I could. I can. So yeah, if you're interested in learning how to do that, I am certainly open to receiving emails or messages or whatever to help you learn how to do that if that interests you. So how to calculate algebra-breakly function inhabitants and so on. Generally how to use types and tests to ensure your software works well. So what I've done here is I've given another example. This is an example of a function. This is written in Haskell. This type here. And I rewritten it in Java. So if you're more familiar with that syntax, given a function from B to C, that's a polymorphic B to C, and a function A to B, return a function A to C, this particular type is also once inhabited. So that is to say there's only one function with this type. Given this type, I already know what it's going to do. But more interesting is, can I prove it? Can I write a proof that this is equal to one? And you'll see that this is a slightly less trivial example than the previous one. And if you start practicing with these trivial examples, eventually you become more fluent in doing it with less trivial examples. Like for example, your great big enterprise system at your office or whatever you might have. Here is a proof of correctness. Go and get a coffee, sit back, enjoy your coffee, the bill's not going to fail. Because there isn't a bill, you have a proof. Wouldn't that be nice? It's a little bit of fantasy there, but that's kind of the aspiration or the thing to aim for at least. Another example here, this one's probably a little bit easier if you chose to go down this path, which is polymorphic A to A to A, or in Java, giving two A's return an A. This is actually equal to two. There's two inhabitants of this type. And because it's not one, that means there's tests to write. So what tests can we write? There's two possible programs with that type. And you can actually prove it using your netLMR that it's equal to two. And here's just to point out that so I wanted to give a less trivial example, just to point out that you can do it. This one, this type here has one inhabitant. And you can prove this one as well. However, you can't write, it's not possible to write this type in Java. So unfortunately, I couldn't convert it. It's just not possible. But it's a good example of a slightly less trivial proof that you could write. But here's another example. And the only difference here is I've changed, this is called a constraint in Haskell, by the way. So all I've done, you know, like an interface constraint. And I've basically changed the constraint that used to say, the previous one said functor and now it says applicative. And all I did is change that constraint. And the number of possible programs has gone from one to infinity. Whoops, that's a big number. So now we've got lots of tests to write. Or is there something else I could do better? I would not recommend trying to prove anything about this particular type. You might ask, you might write a function with this type, and then ask yourself, what tests can I write about this program that I've just written? So that's all I've got. I just noticed there's two minutes remaining. So I'm happy to take any questions or anything like that. Thanks guys for listening. Sorry I couldn't be in India again this year, hopefully next year. Thank you so much, Tony. Thank you for proving my guess absolutely incorrect. Thanks a lot for that. Feel free to find a fifth program if you like. I'm sure there isn't one. Give me the weekend, Tony, and I'll get back to you one day. I'll give you four marks for bravery though. It's a really good idea to at least try, just accept that you're probably not going to get it right. That's okay. That's all part of learning. As you said in the introduction, I do flight instruction. I do it, you know, I'm going to be doing it all weekend this weekend. Mistakes are what my students do all day long, and I do them myself as well. So it's all part of learning. Just be brave and have a go, and learn from those mistakes and get better at what you're doing.