 I'm Chris Nuttycomb, and this talk is about parametricity, which is kind of a big word for a really simple concept. And so just a little bit about me to start with. So I've come to functional programming for over kind of a long road. I had my first professional software development job in 1998. And in 2008, after a decade with Pearl and Java and Ruby, I discovered Scala. And then in 2010, I discovered Scala's Ed. And then I like to say that I kind of fell past the event horizon of the functional black hole. And I haven't written any Scala in the last six months because I've been writing Haskell for basically everything I'm doing. But I've had the really good fortune to lead a few top-notch Scala development teams over time. And I think that's giving me a sort of unique perspective on coming to functional programming. So a little bit about this talk. The first thing that you all ought to know before we go any further is that this is a very introductory talk. It's in some ways one of the sort of two or three things that I think that someone should know on their first day coming into the software world. And so I hope that it's not too basic for any of you. And if any of you have sort of been up in the advanced track Haskell talks today, I'll encourage you to go to Rust or something else like that. Because this is going to be review at best. So a lot of this talk is going to be in Haskell. Although towards the end, I am going to talk about really taking these principles and applying them to programming in whatever language you're working in at the given time. So right now, I'm doing some consulting work. And I'm doing it in Ruby. But I'm still using pure functional programming. I'm still reasoning parametrically. So we're going to talk about types. We're going to talk about universal quantification. We're going to talk about, in the context of universal quantification, what parametricity means. Some a little bit on existential typing. And then, like I mentioned, how we're going to apply these to the work we do in other languages. Also, again, given that this is an introductory sort of talk, if there's anything I move over too quickly or that needs clarification, please stop me. I hope not to leave anyone behind. Because I think that this really is pretty important. So to just get a start, how many of you feel like you have some functional programming experience? OK, so that's a good number. So type functional programming? OK, so for you, this might be a little bit too straightforward. But we'll see. All right, so first thing, types. Types are really simple. When you think about a type, you think about the set of inhabitants, the set of possible values that a term having that type can take on. So the set of possible values for a Boolean is true and false. There are two inhabitants of that set. The set for an integer is, you know, it has two to the 32nd possible inhabitants. Now, when we're thinking about these in terms of sets, one of the important notions is that type is purely a concern of static analysis. When we're working in a dynamically typed language, calling the runtime tags that are checked for verification of whether you can do some particular kind of dispatch is a little bit of a misnomer. Because what we're really talking about with respect to type is these sets that we can reason about before the program ever runs. And I want to make the argument that even if you're working in a dynamically typed language or something where you don't have a type checker to help you, that you are performing this reasoning about types, whether you acknowledge it as such or not. Even if you're working in the most fantastically duct type metaprogrammed fashion, at the end of the day, you always come back to this notion that I can call this function, give it this argument, and it will perform some operation. And the whole idea, from my perspective, behind typed programming is that we can use machines to help us verify this reasoning. I'm not so interested in the notion of compilation as applies to building a binary or something like that. I just want to be able to have a machine help me ensure that when I've made some assumption about the code, that that assumption is valid. All right. So the first thing I'm going to talk about now is universal quantification. And I'm using the Unicode for all just because, especially as you start to read the functional programming literature, a lot of stuff has mathematical notation. And I think it's useful to just get used to seeing this symbol and thinking about what it means without additional crutches. If we get used to using it in our programming language, then when we go and read some paper, then it's easy to translate. And I think it's less so when a lot of the time, especially in unicypes or dynamic languages, we tend to do a lot of our reasoning based upon what things are named. But what universal quantification allows us to do is to escape that dependency on naming. So here we have the identity function. And for all a, we get this function from a to a. So for all, we already said that a type is a set of possible values. So a type variable, something that is declared by this for all expression, that's a set of such sets. So that is the set of all possible types. So we know really nothing about what that type is. If we take that as a primitive, we can say a lot about what this function does. So we can say with absolute certainty that the only way, since we know nothing about the value that is given to it, we have no mechanism to manipulate it. So the only thing that we can do with it is to return it. Yes? Yeah, so notationally, this says for all a, the type of the id function is a to a. And I left id up here because I think everyone knows this function. But from this point on, we're going to use bogus function names so that we don't try to rely upon them for building our intuition about what a function does. All right, so we're going to go back in time a little bit. Quite a ways back in time. I was, I guess, I was 13 when this paper came out. But Wadler said, write down the definition of a polymorphic function. A polymorphic function is one where its type parameters are universally quantified. Tell me its type, but be careful not to let me see the function's definition. And I will tell you a theorem that this function satisfies. All right, so let's see some examples of what we can do with this. This paper, which is called, no, I don't remember exactly. Theorem's for free. Thank you. It is an excellent paper. I'm not going to be presenting the actual derivation in this talk because, again, it's a 101 level. And I want to focus more on the application that we make of this result on a day-to-day basis. All right, so first simple example. Can someone give me a theorem as to what this thing is doing? The output value must be one of the input values. There's no, even though this looks kind of like a function that could maybe combine things or whatnot, we don't have enough information about what A is to say that there is a way to combine an A with another A. And so this not only can it only pick one of the input values to return, but it always has to pick the same one. There's nothing that it can dispatch on to make a choice between them. So here's another simple example. This one in some ways is even simpler. The value returned is always the first argument, and the second argument is ignored entirely. So what's the name of this function? All right, great. This is just the first function, right? You pull the first element out of a tuple and return it, and you ignore the other argument. All right, now these have all been obvious theorems where there's only one solution. But there are some situations where even though we might have something that looks very familiar, what's this function signature? Yeah, this says given a function from A to B, I'll take an array or a list of As and produce a list of Bs. And naively, we might think, well, this is F map. We can say, at very least, I think the strongest thing we can say is that every value in the output is obtained by applying the function provided to a member of the input. But list is not abstracted here, and it gives us too much information. And as a consequence, what we thought was F map may not be. And what I wanted to point out here is really that this result, where all that we're doing is we're picking off the head if there is one and applying the function rather than applying it throughout the list. This is caused by even as simple a data structure as list is, it gives us more information than is really usable in terms of reasoning. It creates an infinite set of possibilities for things that we have to consider when we're trying to reason about this code. This one here? Here, one on the top. OK, so that says, for all A and B, for all types A and B, so A is some arbitrary type. B is some other arbitrary type. We know nothing about either one of them. Given a function from A to B, so just something that converts an A to a B, that's all that we know that we can do is make that conversion, then, and given a list of A values, I can produce a list of B values. Now, in JavaScript, you would think, oh, well, this is map or for each or whatever it's called. But here, because it's list, we're able to deconstruct it and we're able to do something somewhat unexpected. We don't have anything constraining us in this context. All right. And the problem is that when we get list, we have this whole variety of things that we could be doing with it. But let's say that this ASDF function is what we have and that, assuming that it's obeying the typical signature of map, that we want to reuse it. Well, so we're going to say, we're going to assume that it's F map and we're going to write this function based upon lists. And since we have ASDF, we'll just use ASDF in its implementation. So first is a point free version. Or the commented outline is just a point free version of the line above it. But here, we're saying, OK, I'm going to take a list of tuples of, again, undetermined types and I'm going to return just the first elements. Now, we're trying to get some code reuse here by reusing our ASDF function. But we've done two things here. By using ASDF, we've made it excessively restrictive for the caller of our first function that they can only provide us with a list. And like we saw, ASDF is actually broken under the hood. We've been excessively permissive with the implementer who could use any list function that they want because they have that extra information. So what we need to do is we need to, again, universally quantify and instead of using the list type constructor, use another type constructor to restrict the information that we have available. I just used the term type constructor. Is that a term that people know? Or if you don't know that term, raise your hand. OK, I'll just briefly discuss it for those few of you. So when you think about types, a list of integers is a type. You can think about the set of possible lists of integers. It's an infinite set, but every inhabitant of it is a list of the values 1 and 2. There's some specific inhabitant for that. Now, you notice I said list of integers. List is the type constructor. You need the argument of an additional type to get a type. So when I say list of integers, I want to talk just about lists without talking about what's in them. The type constructor here is applied to, for example, a tuple of A and B in this context. But if we want to abstract over it so that we don't have to be concerned with lists, we're going to have to do something else. Anyway, so the title of this talk has to do with information hiding. We want to hide the information that we're working with a list. And so the answer here is type class. I'm going to have to speed up a little bit, because even though I thought this was a short talk, it's going to be longer than I expected. All right. So I talked about type constructors a moment ago. So here you can see this FA. That's saying some arbitrary F. We're going to choose the type list to create an instance of this functor type class. And now the implementation of the functor type class, it could have the same problems as the just bare ASDF function that we were talking about. However, a type class instance is universal within the context of an application. There's only one instance for functor of list within the universe. And what we want to be able to do is we want to be able to apply some laws to constrain us so that we can't do the kinds of abusive things that we can do when we're just defining an ordinary function. So here is our refactored firsts that are depending upon. We're now universally quantifying our F, but we're putting a constraint in place that we have a functor for F. Now contrast this to the case where we had a list. In the case of list, we had a whole slew of functions. Because we've qualified by just functor, we know that the only function we can apply is FMAP. All right, so here's. Oh, that was right here. So functor, again, it's the same type signature as our ASDF. But instead of having the concrete list type constructor, we've used this abstract F type constructor, which we can instantiate to list. But we can also instantiate it to maybe. And we can also instantiate it to IO. And we can also instantiate it to par and whatever other things that we want. So now when we write firsts, we can be sure that we can use anything that satisfies this constraint. And as the user, as the consumer of the firsts type, we now know a lot more about what the implementation of this function cannot do. And this is the essence of information hiding, is excluding possibilities as far as what operations it can perform so that we can reason about the code. All right, so I'm going to actually skip past this. Because this is another sort of similar example. And there are a couple more things I want to get onto later. All right, I'm going to go back to functor for a second. Ideally, what we want to be able to do, and this is something that I was talking to Brian McKenna earlier, who gave the address talk yesterday. And he said, oh yeah, in Idris, we can do this. We can actually put in place laws and say that any instance of this class functor must obey these laws. When we have that level of power, then we actually have the ability to really reason that, for example, you can't do the thing I did earlier where I omitted a bunch of values from the input. All right, so here's an example that we can do in Haskell. This is using doc tests to assert a couple of properties about this reverse function, where we're saying, if we reverse the concatenation of the list, it's the same as concatenating the reversed pieces of the list in reverse order. And then also, reversing the same list is equivalent to, you know, reversing the list twice is equivalent to the original list. So we can get a little bit of the way to starting to express some of these laws in terms of tests. But when I see this definition, I know that the only possible implementation of this function is reverse. There is no other implementation that will satisfy both the properties and the type signature. All right, so where things go wrong with universal quantification? Can anybody tell me what this says? This isn't a constant. This is death. This function will cause your program to fail at runtime. And if we have this in our language, we can't trust anything. So why is that? We're saying, for all a, I can return an a. Now, this would be a magical function, right? If this could exist, it would put us all out of jobs and instantaneously. Because we know from the Curry-Howard ouromorphism that programs are proofs and that programs are values. And if we were able to synthesize any value out of thin air, all we'd have to do is give it the correct type. And then, you know, you'd have Facebook, right? This function defines Facebook. Well, this says all propositions are true. I'm not going to go into that any more deeply. But I'll just give you a minute to read this. It's one of my favorite things. What drives me crazy? This drives me crazy, right? This is something that's in Haskell. And we add a string in there. But it still says that we can synthesize Facebook out of a string now. This is clearly not a true proposition. But having this in our language means that we can't reason about it. So what do we do? Do we give up? Not quite yet. We have some justification for behaving as if we can completely ignore non-termination, completely ignore for all a, a. This is a recent paper that has a really interesting result that you can define a total language. And if you can reason about a function in that total language, that reasoning carries over even when you're working in a language that has non-termination and bottoms. All right, so let's look at some of the common things in our languages that are broken. And that we can tell clearly from the type signature that if we use this function, the head function is in prelude, it's going to fail my program at runtime. Or I have to personally know in my head about some state that the compiler can't know about in order to use this. Same thing here. We have all of these functions in the standard library in what is ostensibly the most widely used or one of the most widely used of the strongly typed functional languages. And they're all just broken. But we can see that they're broken. The important piece there is that we can look at the type and we can know that if I call this function, I might blow up here. I might just fail in an uncontrolled fashion. Yeah. Oh, it's really straightforward, right? List of a, I have enough information in there to know that the list might be empty. But it's as regardless of whether or not I get the empty list, I can synthesize an A. I can synthesize that, you know, that's Facebook, right? So anywhere where you have the possibility of a zero and then you have something that's non-zero coming out, it's obvious to see that this can be broken. All right, I'm gonna also skip back to this because I'm just about out of time and I don't wanna run over. This is just, if you look at my slides later, this is a quick piece about how to do an additional piece of information hiding, which is using a universal constraint to encode an existential. So here we might wanna say something like, we have a foldable of things that have a JSON rendering, but we'd be constrained, since JSON rendering has a type parameter, we'd be constrained to, for the whole foldable, those actually being the same A, whereas if we change where we say for all, we're able to essentially erase that and then we're able to have a foldable just of the JSON renderings and just use the contained information that I've wrapped up an A and a function that can handle an A together and they are now inseparable and so I can always extract the fact that that function can handle that value but I can know nothing else. So, all right, so we can do this in any language. We can do this in typed languages. We can even do this in untyped languages and we do reason about them in the same way irrespective of whether or not we have a type checker they're running to help us. Now, we can define these great type classes that give us, here's the scholar representation of functors and monoids but even in Java we can do monoids but functors now, we're out of luck because we don't have any way in Java of representing type constructors and we now have to descend to doing a bunch of operations in some known type constructor and then lifting back into the type that we're interested in. All right, in Ruby we've just got a lot of ducks but even there, even in duck typing you have that ability to say I'm expecting this thing that I'm passing to respond to this particular method call. It's something I can reason about. All right, so just quick rules. First, give the consumer of the function the most freedom, allow them to use anything that's foldable not just a list and give the implementer the least freedom. Restrict them by the information they have available to only writing the most sensible thing. Never, ever under any circumstances use introspection because if you're introspecting on types then there's always a whole infinite universe of types that you are not handling. This is also the reason why type erasure is a good thing. There are lots of folks who sort of complain about Java that the parametric types are erased when you get to the bytecode. This is actually great because it prevents us from doing awful things by introspecting on those parametric types. It's too bad that we have reflection to introspect on top-level types. And if your language will let you prefer type classes over inheritance because with inheritance in say Java you have the collection at the base of the collections hierarchy and it provides a whole slew of operations. It doesn't allow you to restrict to just the operations that you're dependent upon whereas type classes allow you to be very, very selective. And I think I have not even left time for questions but I'll be happy to talk to anyone else afterwards. So that's it. Thank you.