How do you make a function total?
This is an episode of The Eric Normand Podcast.
Subscribe: RSSApple PodcastsGoogle PlayOvercast
It is easier to reason about total functions. And you can make any pure function total using three techniques!
Transcript
[00:00:00] How do you make a function total?
[00:00:05] Hello, my name is Eric Normand, and this is my podcast. Welcome.
[00:00:12] So there's a concept in functional programming: this idea of a total function. A total function has a return value for all of the valid arguments.
[00:00:28] If you say, I'm gonna take two integers and return an integer, it has to work for every combination of two integers. If you say, ah, well, it doesn't really work for negative numbers. Well now your function is not total and you don't have an answer. An example that everyone knows is division between two numbers. If the divisor is zero, it's not defined. [00:01:00] So what do you do? You have to throw an exception or return something that's not really helpful like a NaN (not a number). That's not a total function. Actually, the term for a function that's not total is a partial function. So division is a partial function.
[00:01:21] Why is this important? Well, total functions are much easier to reason about. You don't have to check their arguments before you pass them to the function to see if it's okay. They act more regularly. And when you have partial functions, you have to keep this in your head about what, what are the valid arguments?
[00:01:49] You can't just rely on the type checker. You have to add more logic.
[00:01:54] And the other thing is when you start having multiple partial functions [00:02:00] that you want to compose, you know, you want to nest them together into an expression, you wanna take the return value from one and pass it to another. And it starts to be really hard to reason about whether your function is going to to be valid.
[00:02:19] And what are the arguments that are valid for this new function that you made? So once you have partial functions, the functions you're making are partial as well. So it's kind of this infectious thing. It's contagious. Once you have a partial function, you're gonna have a partial function when you use it in another function, and then that other function's gonna be used and it's gonna be partial. So it's much better if you can, to make stuff total.
[00:02:50] So how do you make something total? Because you can't change division. Division is what it is. It's undefined what [00:03:00] division by zero means. So how do you do it? Well, there's three ways of taking a partial function and making it total.
[00:03:10] The first one is to restrict the argument.
[00:03:15] Let's say you have a type called integer, and division that comes with your language is defined over integers. It's integer division, so it returns another integer.
[00:03:27] That second argument, the divisor can't be zero. So you can change the type of the argument to be non-zero integer, we'll call it. Okay? And this type you are gonna guarantee somewhere else that it's an integer, but it's not zero. Now your function has a more restricted set of arguments and division is now safe because you're taking a non-zero integer as the [00:04:00] divisor.
[00:04:01] How do you make this type? Well, depends on your type system, but it's probably gonna wrap a regular integer, but do some kind of check where you construct it.
[00:04:14] Another possibility is to augment the return.
[00:04:18] Basically what you're doing is you're taking the error case, the divide by zero, and you're making it a valid kind of return.
[00:04:31] In a language like Haskell, you might use a Maybe, and you can say, we're going to divide these two, maybe. If it's zero, we're not gonna divide them. And so you'll have a Nothing. But if it's non-zero, then we'll return the actual answer, Just the answer. Other languages, call it option, option types, which means you optionally have a [00:05:00] value.
[00:05:00] You could construct a new type that basically does the same thing. Problem with maybe is it's so generic, you don't know what it means when you have a nothing and then nothing's collapse in on each other. If you have multiple operations that are all returning maybe numbers, you don't know which one failed. So this way you could say, Hey, this was a divide by zero. You can give it a name. The problem is then you have a proliferation of types.
[00:05:27] I often do this technique where, let's say I'm writing an HTTP client and there's all sorts of status codes that could happen. I could have a timeout, I could have some failure to do an SSL handshake. There's all these issues that could come. When I'm connecting to an API and I don't want to deal with exceptions, I want to turn all of [00:06:00] those failure cases into part of the domain. So I want to actually return a thing that says this was an error or this was a success and here's the value that I got back from the API or it was a failure and here's the reason.
[00:06:20] And so instead of having like a try-catch, now I can do an if statement, right? And there I can also put other stuff into that return value. So I can also put in something like, can I retry? Is this a retriable failure? Some failures you don't want to retry, and some you do. And so I could have a Boolean value in there that says, should I retry? I could put that into the return value.
[00:06:51] Now the downside is I have to unpack the value, right?[00:07:00] I have to check if it's a success, and if it is, I have to take out the value. And I have to check if it's an error. And if I do, I have to do something about it, either retry or maybe eventually, I do want to throw an exception, right? But I can do this because I've made my HTTP library total. So that's a another way of augmenting the return: enfolding the error states into the function.
[00:07:29] Another very important time I do this is on the client. Like in the browser, I have to show the failure state in the UI. The failure is a valid state to show. I don't want to handle exceptions and things. I want it to be part of the value that I'm showing. I want [00:08:00] to talk about how to do this in the book. anything I do with Ajax, I will make it return something like, It's a success. Status is success. Here's the value. And if it looks like that, then the UI will just display the value. Status is loading, so then it'll display a spinner. And status is error, and it will, you know, display an error . Try again. I've incorporated the error states into the return value.
[00:08:38] Gonna go through the first two again, and then I'll say the third one. So we've seen how to restrict arguments and we've seen how to augment the return value.
[00:08:49] The third one is to change the meaning of the function.
[00:08:56] I've given this example already a few episodes ago [00:09:00] where you could have an error. Let's say you have a hashmap, and you wanna remove a key. But that key doesn't exist in the hashmap. If you say, remove this key, and it's not in there, they will throw an error. Some languages do that. Other languages make a different choice, which is they say, Well if you wanna remove it, and it's not in there, then that's fine. We're just not gonna modify it.
[00:09:35] And the semantics is slightly different. It's give me a version of this map that doesn't have this key. If it already doesn't have this key, the versions are the same. If it does have the key, of course you're gonna return a new version without the key.
[00:09:54] Likewise, you could do a string replace: Replace all of the letter X with the [00:10:00] letter F, something silly like that. Well, you could say if it, if it doesn't have an X in it, throw an exception. You could do that. Some languages might actually do that as part of their standard library. But then you have to do the check. Does it have an X? If it doesn't have an, if it doesn't have an X, then do nothing. If it does have an X, now do the replace.
[00:10:27] You're gonna do that check anyway. You might as well incorporate it into the meaning of the function. So you change the meaning of the function. You change what it means to fail to find the thing in there. It's now total. All valid strings make sense. An empty string works, a string without this letter and a string with the letter.
[00:10:52] And so that's something that I also will do: Think, Can I change the meaning of this function so [00:11:00] that it always makes sense?
[00:11:02] One thing I remember from college was we had the Fibonacci sequence. The standard way of defining them doesn't really work for negative numbers, but my teacher showed that if you squint at the formula and the sequence, you can kind of see how you might write it. So it works for negative. And we wrote it for negative numbers. And so now this means that it works for any integer because it works for positive, negative, and zero. And it's just another way of looking at the function so that it works in all cases.
[00:11:42] Total functions are super important, and I'm gonna spend a lot of time on them, especially when we're talking about operational stuff. They're much easier to reason. And to make clean abstractions on top of and more straightforward code.
[00:11:57] My name is Eric Normand. This has been [00:12:00] another episode of my podcast. Thanks for listening and as always, rock on!