Join Kit Eason for an in-depth discussion in this video Struct records, part of What's New in Visual Studio 2017 for F# For Developers.
- [Instructor] Next in our journey into struct items in Visual F# 4.1 we're going to look at struct records. So here we're looking at, some, again, perfectly ordinary F# pre 4.1 code where we're declaring a record type from lines seven through 11 called TreasureLocation. And it's got two fields, number of paces north from some known point to where we're going to find the treasure, and the number of paces east. And I've taken an arbitrary decision that we can only do whole paces so they're integers.
So if you know F# you'll be very familiar with that, but just as a little reminder we're going to send that to F# Interactive. And there we go TreasureLocation, PacesNorth, PacesEast, and a couple of values. So you can see on line 13, we can instantiate an instance of this record type just by putting some names of the fields and their values with equal signs, the whole lot separated by a semicolon, and then in curly brackets.
So we don't actually use the TreasureLocation type name we just allocate the field values and that's good enough. So completely standard pre F# 4.1. What we want to gain the potential performance improvements of allocating those on the stack, well, it's slightly different from the way we handle tuttles. We use an attribute called struct, and that's all we have to do. Send that to F# Interactive again, and ignoring performance, nothing much has changed.
We instantiate the record in exactly the same way. In a moment, as we did with tuttles, we'll look at the performance implications in practical terms, but before we go ahead and get excited about that there are some things to bear in mind. Let me just bring some other code into view. Let's say we want to be a little bit unfunctional and make the fields of our record type mutable. Just as revision, remember, if we declare something's mutable, it means we can update it after it's been instantiated. It's kind of against the F# functional paradigm, but it can be very useful in certain situations.
So, here we have a struct record called MutableTreasureLocation, and as you can see on line 21 and 22, both the fields of that are mutable. And we instantiate it in exactly the same way on line 25 except just to disambiguate that from the earlier record type example I've said MutableTreasureLocation.PacesNorth and that pins it down to be the record type we want. That all works fine. You can see there's no compiler errors on the screen, but what happens when I actually try and allocate a new value to the PacesNorth field? You'll see on line 25 I said PacesNorth were 99, I'm going to change my mind on line 28, and say PacesNorth was 98.
Well, it's a compiler error. I'm going to hover over that for you. A value must be mutable in order to mutate the contents, or take the address of a value type. So, it's mutating the contents which is the culprit here. Luckily it's a very easy fix. On line 25 I've edited that so the actual instantiation of moveableChest itself was mutable. So the fields have got to be mutable, and the actual value's got to be mutable. And if you think of the way in which things are done on the stack that actually does make sense.
Now onto a somewhat subtler issue, and you may want to kind of let this wash over you if you're not particularly interested, but I'm just, for completeness, I'm going to say a little bit about cyclic references. So here on line 34 through 37, we've got a record type, in fact two record types, which are mutually referential. That is to say I've declared Rec1 as a completely ordinary record type. I happen to have made it R2 field mutable, and I've made it a Rec2 option, but I haven't declared Rec2 yet.
I do that on line 37, and of course, you probably know from existing versions of F# pre 4.1 that if you want to declare a couple of things as mutually referential you just use the and keyword. So, at line 37 says and Rec2 = and then we've got a definition for Rec2 which references Rec1. So basically we've got a pair of record types which reference each other. For the life of me I can't think of why you'd want to do that, but I guess there might be some scenarios perhaps a little more complicated than I've set here, where you'd want to do something and now adjust to that.
So let's play around a little bit with this. The first thing to say is we can't put a struct attribute on this second item, so the thing as a whole can be a struct, but the individual elements can't be. Which gives us a little bit of a problem when we come to actually instantiate this, if you're thinking ahead and you really understand what's going on. It's actually quite tricky to create instances. I've had to make a mutable R1 and initialize it's R2 value initially to none, because, of course, I haven't yet declared an R2 instance.
And then I've made an R2 which contains an R1 value of none, then I've gone ahead here on line 42 and now I've got an R2 value, I can go and change the R2 field of the R1 instance, I did say this was a little involved and strange, to the value we declared on line 40. And here, on line 45, I've done the kind of, the mirror image of that and I've assigned the R1 value of the R2 instance to Some R1.
So you can kind of get through this cyclic references limitation but the thing as a whole isn't all going to be allocated, I suspect, on the heap. So, if you're designing some complicated scenario with records that need to be cyclically referential, you need to think a little bit about the performance implications and your design. Another limitation as I've said in the comments on line 48, you can't call the default constructor for struct records. So here on 59 I'm saying let myPointRec, and I'm calling, something which will exist internally, one suspects, a Point3DRec constructor, hoping its x, y, and z values will get some kind of default value, but no dice, that's a compiler error.
I hover over it, it's saying no constructors are available for the type Point3DRec. That's hidden from you. If you're an accomplished F# developer, you're really going to be phased by that. It's not the kind of thing you want to be doing anyway. You want to be doing the value assignment method of instantiating records. Right, we're going to get subtle again, when marked with the CliMutable attribute the struct record will not create a default constructor.
Now, I looked at that limitation, and I thought, this is going to give us a problem with json serialization and deserialization, because certainly, historically, that has sometimes relied on default constructors to create instances of records, and only later populate them with the stuff it's parsed out of the json. So I did a little bit of investigation on this, so here we've got a record type Point3DRecCliMutableJson, (laughing) with x, y, and z fields.
And I've got the CliMutable attribute, which is something you often need to interact with other code bases when you're using F# records, because it basically lets those other code bases instantiate the record type, and only later populate its values. So let's have a little experiment. Here on line 79 I'm instantiating an instance of my Point3DRecCliMutableJson, with values of one, two, and three. Then on line 81, I'm serializing it into this string.
Then I'm deserializing that string value into something called p3dResurrected. So let's experiment with that, and let's send that all to F# Interactive. And we will expect p3dResurrected to contain one, two and three, wouldn't we? Just take a moment to convince yourself that I'm right, and to send this to F# Interactive I'm just going to need to make sure we've got the mutants off the json library available.
So I'm just going to right-click, Send to F# Interactive in time honored fashion. Just make sure that's happened, it has. So I'm going to send this whole section of code, including opening the mutants off json, namespace, and the struct attribute on 67, and the CliMutableAttribute on 71, all to Interactive. And the good news is, our p3dResurrected thing which, remember, got serialized to json and then deserialized from json has got the one, two, and three values.
But, let's see what happens if we don't quite cook the pudding right. Let's take the CliMutableAttribute off, send the whole thing again, and just for safety I'm going to reset Interactive, and I'm going to resend mutants off json, just so we've got a clean setup there. So there you can see our string representation is a pretty reasonable representation of the record type.
But what happens when we try and deserialize that again? Oh dear, x, y and z are all zero. Unusually for F#, your code is going to compile but it's going to fail in mysterious way at runtime. So the takeaway is, if you're dealing with struct record types, and you want to serialize them, and deserialize them, make them CliMutable.
To be honest, I haven't experimented with the performance implications of that, so, if you're in that scenario, make jolly sure you've done some experimentation. Finally, as promised, let's have a little look at performance. This section is exactly like the equivalent thing we did for tupples and struct tupples a little bit earlier. So, here we've got a simple record. We've got a count, we're initiating an array that is count long, and contains instances of our record type.
And by the way, down here, I've just had to cast our i values to floats. I'll just show you the end of the line to make sure you know there's no trickery there, so that's completely standard F#. So we're going to send that to F# Interactive, having turned on timing, and we're going to call it. And we're going to observe that that takes one second and 749 milliseconds.
Remember, that is the non struct version. On we go to the struct version. And the only difference, really, in the code is here at line 124, we've put the struct attribute on, and we've made sure that it's that that we instantiate. 119 milliseconds. So I'll back to back that, reference type, value type.
So that's 1.184 seconds in the case of the reference type, and 185 milliseconds in the case of the value type. I'll reiterate that's not a guaranteed performance gain, you do need to think about the context, but also, it can be a quick win, because there's very little syntactic overload in making that change. So, a very quick recap, to make a record a struct, you just put the struct attribute on it. You bear in mind that if you want a mutable instance, you're going to have to not only declare it's fields as mutable, as on line 21, 22, you're going to need to make the instance mutable, as on line 25.
You'll want to be a bit careful about cyclic references, but that's a real design smell anyway, so we'll gloss over that. You can't call the default constructor for struct records, but that's no big deal if you're writing idiomatic F#. But, finally, you need to be super careful around serialization and if it's going to be a struct, make it a CliMutable to guarantee correct deserialization.
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