Advanced Elm Opaque Types Solution
Transcript from the "Opaque Types Solution" Lesson
>> Richard Feldman: Let's go through the solution. So the goal here is to make a cred an opaque type. Let's see how we can do that. So the first thing I'm gonna do is change this from type alias to type, and I'm gonna need to give it some sort of variant here, so I'm just gonna name that cred.
[00:00:14] So we're gonna have type Cred = Cred. And then at this point I have a record for username and token. But honestly, records are most often useful in my experience when used within context of a type I'm going to make opaque. If I have different fields with the same types or I have quite a few different fields.
[00:00:32] In this case, I only have two fields here, I have two pieces of data. So I'm just gonna go ahead and say, Username String and cut directly to the chase. We can see elsewhere in this project where I didn't do this, where I actually did use record insider, but for this one I'm not gonna bother.
[00:00:48] Either way works. If you did that way and you kept the record in there, that's totally fine, but I'm gonna demonstrate solving it this way. Okay, so let's see if this compiles. Shocking, it didn't. Okay, so we have some errors. And immediately we have errors, even though we were compiling targeting main, the first errors that we see are actually in the module that we're editing itself.
[00:01:07] So it says, TYPE MISMATCH, this is not a record, so it has no fields to access, which is true cuz we stopped making it be a record. So first thing we need to use, we need to have access cred.token. I'm just gonna destructure this inline and say, Cred token.
[00:01:22] And now that should, oops sorry, and I'm gonna need to do an underscore for the other one. This is one of the downsides of doing it this way instead of doing it with a record. If I'd done it with a record, I could go like this and then no matter how many fields I had, I would be able to pull the token off of it.
[00:01:36] If you haven't seen that syntax, by the way, that's inline record destructuring. That would work if I had made it so that there was a record inside there, but since I didn't, I'm gonna need to do the underscore. Again, there are trade offs to doing it either way, but I'm choosing to do it this way.
[00:01:51] Okay, let's see if that fixed that one. Okay, nope, not happy about that. I see, because token is the other field. So t one of the downsides of doing this is you can potentially mix those up. Fortunately, if you do mix it up, the compiler will tell you, as long as they have different types, which I only do this because these are different types.
[00:02:11] If those were both strings, I absolutely would give them names, because otherwise, I might make the mistake that I just made. Okay, looking up here, we have encode.string, cred.token. So once again, we want to get Cred _ token. And, whoops,
>> Richard Feldman: tokeben, okay, so now we're done resolving the type errors within this module.
[00:02:33] We're on to the next module, which is viewer, okay? So, it's got the same concern here, which is that info.cred.username does not exist because cred is now an opaque type. So, since, sorry, since it's not a record anymore, I can't save.username directly. But what I can do is I can expose something called Username, like a function called Username, which exposes that directly.
[00:02:56] Now, that's something that is totally fine to expose. The whole point of this was to make it so that they can't see the token, but it's okay if we expose the username. So the way we're gonna do that is I'm just gonna add something here that says INFO.
[00:03:08] This is usually where I will put some thing that sort of the read-only section. Stuff that I'm choosing to expose that does not actually result in a new cred value being exposed for the particular opaque type. That's what I usually tend to mean by that heading. So I'm gonna call it username.
[00:03:22] I'm gonna say username takes a Cred and returns a String. So cred =, and at this point, since Cred is a custom type, I'm actually gonna wanna destructure it inline, and do that. Calling it uname instead of username to avoid the name conflict with the llll name of the function itself.
[00:03:44] And then I need to remember to expose that. Okay, so now we have a way for others to access it. All they need to do is say Cred.username, and then pass the cred value, and then now they have access to that username. Let's see what that does. Okay, oops nope, I said that, [LAUGH] that's my mistake.
[00:04:03] I said that it was gonna return a string, but in fact the type of the username is in fact username, it's another opaque type. So this is inaccurate, this should actually be Cred to Username. Okay, there we go, and finally we're down to the pages themselves. So pagesettings, here we have 65, Cred.username cred.
[00:04:29] We have page.elm line, this would be easier if I had my integration turned out, but I don't.
>> Richard Feldman: cred, [INAUDIBLE]. All right, so here I'm trying to destructure this inline, but that's not going to work anymore because it's not a record, so Username.cred. Sorry, backwards Cred.username, okay. And then now username can be that.
[00:04:56] Let's see what we have left. Both in author 223.
>> Richard Feldman: Okay.
>> Richard Feldman: And the last one, 98.
>> Richard Feldman: Okay.
>> Richard Feldman: There was one more, pageprofile.
>> Richard Feldman: 260.
>> Richard Feldman: There we go, okay. So now with a few mechanical transformations that took maybe a minute, we have now created the guarantee that across our entire system nobody knows what that token is except for Cred itself.
[00:05:37] That implementation detail is completely hidden, and if we ever decide to change the format of the token, we know we can do that without causing any regressions. One thing to note about that is, as you've seen, if your currently exposing something and it's not opaque, and you realize, hey, I could make some more guarantees, and it would be a little bit nicer to maintain if I made this opaque, that's a transformation you can do after the fact.
[00:05:56] One of the nice things about doing it this way is that you'll immediately encounter all the scenarios where you were aligned on the implementation details. And if there are any problem cases where you're like, uh-oh, we really genuinely are relying on this and it's a problem, you'll at least know about it and have the potential to be able to fix it.
[00:06:11] So this is one of those things where it's never too late. If you change your mind later and realize, you know what, I actually could make this opaque and that would be better, you can do it after the fact. A lot of these techniques that we're gonna learn today are things that you don't need to feel bad about not having done.
[00:06:25] This is Elm, refactoring tends to be pretty easy, pretty reliable, so you can always go back and make changes after you learn a new technique, yeah?
>> Speaker 2: Okay so before we were exposing Cred and then parens, and then exposing within there, that was because it was a union type, or I mean a-
>> Richard Feldman: Custom type.
>> Speaker 2: Custom type.
>> Richard Feldman: So you mean like this, oops?
>> Speaker 2: Yeah, like we would've put username, say within there?
>> Richard Feldman: So when we did it this way, then that would mean that other modules would be able to call this variant right here and make Cred by hand.
>> Speaker 2: But you couldn't put just one thing within the parens?
>> Richard Feldman: I forget if you still can. The syntax of that changed recently on the import side. Yeah, you can't. You have to either expose all of them or none of them. Those are the two options. [LAUGH]
>> Speaker 2: And then, username just is getting exposed via cred because the compiler sees that we're using it as it's signature, or?
>> Richard Feldman: No, I just made a function called username that extracts it and returns it, and I'm exposing this function by hand. So once you make something opaque, all access to any of its internals become completely opt-in. And this is the way that would you opt-in. Is you would expose the function that says, here, now you can have access to this.
[00:07:55] Just call this function and you'll get it.
>> Speaker 2: And we're accessing it off of Cred in the other module because that's what we imported the module as?
>> Richard Feldman: Yeah, yeah, yeah, right. So the other modules have import Viewer.Cred as Cred. Yeah, just as a shorthand. Yeah, good question.
[00:08:12] Worth noting, by the way, that when I used to be a Java programmer, we had these things called getters and setters, which kinda resembled this, where you would just do this all the time for everything. And you'd also expose a function that would modify, which defeats the entire purpose of all this, so I would highly recommend against doing that.
[00:08:29] But rather think about in terms of, okay, what's the minimal set of things that I can expose to make my program work? And how can I selectively expose them one at a time on an as needed basis and not just be like, well, I'm gonna make it technically hidden but then I'll just expose all of it using functions?
[00:08:48] That's just a bunch of [LAUGH] extra work for no benefit.
>> Richard Feldman: Yeah?
>> Speaker 3: So it feels kind of implicit, that in the type of Cred, String represents a token, could we make a type for token that-
>> Richard Feldman: Yeah, absolutely, for sure.
>> Speaker 3: Would that be going too far, or?
>> Richard Feldman: No not at all, I mean, honestly, I think that would be totally fine, like AuthToken.
>> Speaker 3: [CROSSTALK] mutation.
>> Richard Feldman: Absolutely, yeah, no, I think that's totally fine. The only reason that I didn't was because it only lives in this module. So I was like, I'm not really worried about other modules finding out about it.
[00:09:27] But yeah, I think that's totally fine. I mean, I could see doing even something like type alias token. If it's just gonna be for this module, I can do it that way. Or I could even see as far as just saying, yeah, you know what, [LAUGH] I'm gonna straight up do it that way.
[00:09:41] You could even give it its own module and make it so that Cred is the only consumer of that module. There's a number of ways you could do it. But I think given that this is an implementation detail that lives within this module anyway, I think there's not gonna be a whole lot of benefit to doing that.
>> Speaker 3: There's only like ten lines of code, so-
>> Richard Feldman: Yeah, I mean, Cred's not doing a whole lot, so, [LAUGH].