Join Kit Eason for an in-depth discussion in this video The "result" type, part of What's New in Visual Studio 2017 for F# For Developers.
- [Narrator] In this chapter we're going to look at a thing called the result type. Now, the result type, sometimes known as the choice type, has been in and around the FSharp ecosystem for quite a while. It's a way of handling errors in a nice kind of way, and people have been hand-crafting code to deal with it. But, honestly, you don't need a great deal of additional code to do this, but the decision has been made to bring the stuff into the FSharp libraries so you can do it without any external code. The other thing to say right off the bat is that you're not obliged to use the result or choice type.
It is a technique and I'll signpost, too, at the end of the course to some resources which will let you get started with it properly, conceptually, but because it is a new FSharp 4.1 it is important you understand it's there and why it's there. And we're going to do that in several different sections. Firstly, we'll look at the result type itself and why you might want it with a little bit of code. Then we're going to look at some functions which the standard FSharp 4.1 libraries provide you with for leveraging and using the result type. And then in the next video we'll also look at the result type in this thing called Railway Oriented Programming, which is just a way of conceptualizing usage of the result type.
Let's take a step back and think about how typically we handle errors in our programming. We're going to look at the problem of taking a number, which I've called n for numerator, and another number called d for denominator, and dividing n by d. And you don't need a great deal a mathematical background to know that if d is zero, you're going to get a problem because that's an infinite value. And there are various ways of handling that. First take what a very naive divide function at line nine, which takes n and d and just divides n by d.
Let's try that out. Send it to Interactive. Divide 4.0, don't really need to type the naught, actually. And that works perfectly. What happens, though, when we divide by zero? Well, the way .NET behaves, and this is very nice, is it returns infinity, which, mathematically, is what we've got. But that might cause you problems down the line, depending on exactly what is consuming this value. So, let's look at some other options.
Let's make a design decision that we want to raise an acception saying divide by zero if we divide n by zero. Let's check that works. I'm just going to send open System to FSharp Interactive. And then I'll divide with exception. So, now, divideWithException. Let's go down the happy path first. Returns two.
Raises an exception. Now, some people say, and I'm not sure I entirely disagree with them, that, effectively, this means that the type signature of divideWithException, which, if we look up here, is that it takes a float and another float and returns a float, that type signature is a lie because, yes, it sometimes returns a float, and it sometimes raises an exception. And that lie kind of leaks out through your code base and you can never quite rely on what functions like this are doing. They have a kind of Janus-like face, sometimes they will be happy and they will give you something nice, sometimes they will give you a completely different kind of thing, which is an exception.
So, how do we begin to tackle that problem? Well, we have the try pattern where, in the unhappy path here at line 19 and 20 we return none, and in the happy path we do the calculation and we wrap that up in a some, bearing in mind that each branch of the if statement is going to need to return an option type. If one of the branches does, it means we have to go into some, here. Try that out. Happy path first.
Some 2.0. None. And that's a well-established practice. The issue with it is you have no way of communicating with the outside world what went wrong. If there are several different ways in which your operation can fail, the caller can't really tell which one of those happened because it's just getting a none. And the result type is a way of addressing that basically by putting a payload on the unhappy path. So, let's look at choiceDivide and that saying at line 25 then equivalently to line 20 where we return none, here we're returning Result.Error, and then a string, divide by zero.
And the result type here, which sits, as you can see, in Microsoft.FSharp.Core, is the main thing which is new in the 4.1 libraries. If we look carefully at the type signature, it's type result, some Type, that's what the type T means, and some Error, which means the unhappy and the happy paths can have different payloads. In this case the happy path payload is the floating point result, and the unhappy path payload is an error message. That unhappy path payload might be some other type like an exception or a numeric error code or basically whatever you like because the result type is generic for both its happy path payload and its unhappy path payload.
And you can see the cases are Ok of ResultValue of type T and Error of ErrorValue type T error. So, continuing back to our concrete code in the else branch, we're returning n divided by d and sending that into the Result.Ok case. So, it's very, very similar to lines 18 through 22, except instead of using none and some, we're using Result.Error and Result.Ok and the error path has a payload which, in this case, we've chosen to make a message.
Try it out. Happy path first. Okay.2.0. Error Divide by zero. You can see I've also provided basically the same cases I just ran in FSharp Interactive in the sample code so you can very easily run them yourself. Let's just take a moment to examine the return types of these items.
Here with the straight naive divide we've got a float, which might be infinity, and that, as I say, might be fine depending on how you're consuming the value. Here we've got, again, a value of float and that tells us that, effectively, the type of divideWithException is a lie, the type signature because it's not always returning a float, sometimes it's throwing an exception. Here we've got a float option in the tryDivide idiom. And here, very similarly, we've got a result of float and string.
Remember that the concrete types of the result payloads, in this case float and string, are determined by type inference based on the fact that we actually hardwired a string there. And, in fact, the type inference is going from the fact that we're comparing a zero there to know that n and d are floating points, and that the payload of the result type is also a floating point. That's all fine and dandy but we've got to also think about it from the point of view of the consumer of these results.
Let's say the consumer is just something that print out the result. So, we've got a showDivide function here, we'll try it out. Pleased to quote fine. Unhappy path. Well, that's a bit concerning, isn't it because that's actually showed us a zero.
Let's try the exception one. I'll just do the unhappy path to save a bit of time. We get an exception. So, the exception's kind of leaked out of this layer of the code which may or may not be a good thing. Here we're having to pattern match. So, this is a good thing, actually, because it means you're forcing the caller to consider what might come back from the function that it's calling.
And, at first, this seems a real nuisance, and later on you realize it's forcing you to think about unhappy paths right up the stack, and that eliminates huge classes of bugs. Try it out. Again, I'll just do the unhappy path. And we get a message saying couldn't calculate because we went down that branch of the pattern match because we got a none back from tryDivide and we told the software what to do with that case.
How do we extend that to the new result type? Well, it's very similar, we pattern match, except our pattern match cases are Ok and Error and x gets populated with our happy path floating point result, and m gets populated with our unhappy path error message. Try it out.
Couldn't calculate, and look, this is quite exciting. We now know what went wrong. And the reason we know what went wrong is because, unlike the none case here, the Error case has a payload which you can then recover using the pattern matching and using some kind of error handling, in this case, just printing out what went wrong. And that's absolutely fine. There's another possibility, though, and that is using the functions which FSharp 4.1 call library provides you for, in a more general way, doing what we did at line 47 through 50.
We've got two functions to do that, well, in fact, three, but we'll come onto the third of those in a second. We've got Result.map here, and you'll see that is in Microsoft.FSharp.Core. And what Result.map does is recover for you the happy path value. And then we've got Result.mapError which recovers for you, in this case, the error message which came back. And you simply pipe them together like this and the Result.map will cause your lambda function to get called in the happy path, but the unhappy path results will just kind of wiz past this stage, and it just won't affect this stage at all, not even in terms of type.
So, we can't see within the scope of that lambda, we can't see the Ok or the Error wrapper. It's unwrapped for us and provided unwrapped to our lambda function. And this, by the way, is why some people call this rail-oriented programming. If you visualize kind of a points system in terms of the way railway lines branch, we've branched off for the unhappy path which means that our happy path branch of the railway simply doesn't see problems. Here is something very analogous, except that instead of providing the happy path result to its lambda, we provide the unhappy path result.
We'll try it out. And, as you might expect, that returns, or it prints Divide by zero, but also, you'll see it actually has a result type so we'll need to deal, in a later stage, with how the results get passed on further down the line.
So, to reiterate, the result type is very, very similar to the some type. The only real difference is that the unhappy path has a payload which you can then recover either by pattern matching like this, or by using the built-in map and mapError functions like this. In the next video we'll tie up some of the loose ends.
Kit Eason discusses the new value types that provide an opportunity for performance gains, the new result type which gives you access to the railway oriented programming style of error handling, and program organization and readability changes. Plus, he explores the evolution of tooling for F#, and explains how F# tooling has changed in Visual Studio 2017. To wrap up the course, he shares how you can contribute to the F# language and tooling by getting involved in the open-source community.
- Working with struct tuples
- Marking a record type as a struct value
- Marking a discriminated union as a struct type
- Using the fixed keyword to mark a value
- F# result type and associated functions
- Resolving potential naming clashes between modules and types
- Error message improvements
- The past and future of visual F# tooling in Visual Studio
- Reviewing F# tooling changes