spotifyovercastrssapple-podcasts

Intro to Opaque Types

Opaque Types are a fancy way of saying "custom type with a private constructor." We talk about the basics, how to get started, and some patterns for using Opaque Types.
April 3, 2020
#2

Opaque Types

Some patterns

  • Runtime validations - conditionally return type, wrapped in Result or Maybe
  • Guarantee constraints through the exposed API of the module (like PositiveInteger or AuthToken examples)

Package-Opaque Modules

Example - the Element type in elm-ui.
Definition of the Element type alias

elm-ui's elm.json file does not expose the internal module where the real Element type is defined.

Example from elm-graphql codebase - CamelCaseName opaque type

Transcript

[00:00:00]
Hello, Jeroen.
[00:00:02]
Hello, Dillon.
[00:00:03]
How are you doing today?
[00:00:04]
I'm doing great.
[00:00:05]
How are you?
[00:00:06]
Very good.
[00:00:07]
I'm ready to dive into this topic.
[00:00:08]
What are we talking about?
[00:00:09]
We're talking about opaque types today.
[00:00:10]
Since we're going to talk about types a lot during this podcast, I think we need to set
[00:00:19]
up a good baseline with talking about opaque types.
[00:00:23]
Yeah.
[00:00:24]
That's a great point.
[00:00:25]
It's a big topic and I don't think we're going to be able to cover everything about opaque
[00:00:30]
types.
[00:00:31]
I think this is really just setting it up for many future episodes to explore this topic
[00:00:36]
further.
[00:00:37]
This is going to be opaque types episode one out of some amount of episodes.
[00:00:43]
Yeah.
[00:00:44]
I think this is like a whole...
[00:00:46]
There's going to be a whole college curriculum where this is opaque types 101, but hopefully
[00:00:51]
we can get a 201 and a 301.
[00:00:55]
Or more specified episodes.
[00:00:57]
Yes.
[00:00:58]
Yeah.
[00:00:59]
Opaque types about phantom types, opaque types about validation, for instance.
[00:01:05]
Exactly.
[00:01:06]
Yeah.
[00:01:07]
Opaque types is really just one of the core tools in your design toolkit as an API designer
[00:01:14]
in Elm.
[00:01:15]
When I say API designer, I don't just mean designing packages.
[00:01:21]
Of course you're using your API design toolkit more than any other time when you're designing
[00:01:25]
an Elm package.
[00:01:27]
But really if you're just building something at your workplace or something in a small
[00:01:34]
project that you're maintaining, you still use API design to make your code easier to
[00:01:38]
work with.
[00:01:39]
Yeah.
[00:01:40]
You use API design a lot when writing an application.
[00:01:43]
Definitely.
[00:01:44]
Yes.
[00:01:45]
Exactly.
[00:01:46]
So how about a definition?
[00:01:48]
What is an opaque type?
[00:01:51]
What I would say is an opaque type is a custom type or a union type in other languages where
[00:01:56]
the constructors are not exposed to other modules.
[00:02:00]
Exactly.
[00:02:02]
That's it.
[00:02:05]
That's exactly how I think of it.
[00:02:09]
It's just a really fancy sounding term for saying that it is a private constructor.
[00:02:14]
Yeah.
[00:02:15]
So fancy.
[00:02:16]
The private constructor for a custom type.
[00:02:18]
There you go.
[00:02:19]
It's not that mouthful but less mysterious.
[00:02:23]
Yeah, exactly.
[00:02:25]
So what would be the most basic example of an opaque type?
[00:02:34]
I've got a go to example that I use.
[00:02:37]
So one thing I use is there are certain constraints that you can't do simply using an Elm type
[00:02:44]
by itself.
[00:02:45]
You need a type that is private and therefore you can ensure certain constraints about that
[00:02:52]
type through the module API.
[00:02:55]
Yes, definitely.
[00:02:56]
So for example, let's say that you wanted to have a positive integer.
[00:03:04]
Elm has no way to enforce that an integer is positive because you don't have dependent
[00:03:08]
types or any fancy stuff that checks the values through the type system.
[00:03:14]
Yes, exactly.
[00:03:15]
So you can say something is an int but you can't say that it's a positive int.
[00:03:19]
So how would you do that using Elm?
[00:03:23]
How would you enforce that constraint?
[00:03:26]
You would make an opaque type.
[00:03:28]
So you would create a new type positive integer.
[00:03:31]
You would put it in a different module and you would not expose the constructors.
[00:03:37]
And then you would expose one function or at least one function that would be able to
[00:03:42]
create one such positive integer.
[00:03:46]
And then one would check whether the number is positive.
[00:03:51]
And if it isn't, it doesn't return a positive integer, it returns something else.
[00:03:55]
So you could have a maybe positive integer or a result with a more precise error.
[00:04:03]
Right.
[00:04:04]
Now, okay, so this is bringing up an interesting point because there are different patterns
[00:04:10]
for using opaque types to model these constraints.
[00:04:13]
So the one you're talking about is sort of like modeling.
[00:04:18]
It's modeling a constraint through a type that represents a runtime check.
[00:04:25]
So there's this runtime check if n is greater than zero or greater than or equal to zero,
[00:04:31]
depending on how you want to define positive.
[00:04:34]
Let's just say greater than zero.
[00:04:37]
Okay, so you can check if n is greater than zero, then you give just positive integer
[00:04:46]
n.
[00:04:47]
Yes.
[00:04:48]
Otherwise, you return nothing.
[00:04:51]
That's one pattern that you can do to use a runtime check that you could validate something.
[00:04:55]
You could say, is something a valid email address and do some runtime check or things
[00:05:00]
like that.
[00:05:01]
Right.
[00:05:02]
You can do any sort of validation.
[00:05:03]
Is something a strong password?
[00:05:04]
Exactly.
[00:05:05]
Right.
[00:05:06]
And so if you have that type, you know that it has met the validation that you define
[00:05:12]
in that module.
[00:05:13]
And you know, the only place to look to see what the constraints are is within the boundaries
[00:05:19]
of that module.
[00:05:20]
So it makes it easier to reason about the code because you don't have to think about
[00:05:23]
every place that's calling that module.
[00:05:26]
You don't have to think about every place that might in the future call that module.
[00:05:30]
You only think about the code in that module because it's the gatekeeper for that constraint.
[00:05:35]
Yes, you don't have to think about is the age of the user above zero in the user admin
[00:05:41]
panel, for instance.
[00:05:44]
Yeah.
[00:05:45]
You only have to think about it in one location, which is the positive integer module.
[00:05:50]
Right.
[00:05:51]
So this is a great technique for taking a runtime check and turning it into typed data.
[00:05:59]
And there's this sort of process when you're working with Elm in general that you're always
[00:06:04]
sort of refining down a type as you go along.
[00:06:08]
You're proving more and more qualities about that data.
[00:06:12]
And so keeping track of a certain constraint or validation is really useful with a type.
[00:06:19]
You could also say, you know, this is a non empty string and you could represent that
[00:06:25]
using this sort of technique of keeping track of a runtime validation through a type that
[00:06:31]
is returned conditionally.
[00:06:33]
So you will only return that type on the condition that that validation passes.
[00:06:37]
Yes.
[00:06:38]
There's another way to do it.
[00:06:41]
So let's take the positive integer example.
[00:06:43]
We talked about doing this runtime validation where we conditionally return that type.
[00:06:48]
And so if we have that type, we know that condition was met.
[00:06:51]
There's another way to do it where you don't have to return that type conditionally using
[00:06:56]
a maybe or a result.
[00:06:59]
The way you do that is you say positive integer, you define a constructor one that gives you
[00:07:07]
a positive integer one and within the API for that positive model, you know that one
[00:07:16]
is a positive int.
[00:07:17]
Yes.
[00:07:18]
And then you could you could expose whatever API you want.
[00:07:22]
And of course, it depends on your domain.
[00:07:24]
It depends on the problem you're trying to solve.
[00:07:26]
But for example, you could have an increment function that increments it by one.
[00:07:32]
Now you still are guaranteeing that it's a positive int.
[00:07:36]
So you're not doing a validation at runtime that will conditionally return you a type.
[00:07:42]
You are proving through the API that it's impossible to not meet that constraint because
[00:07:49]
of the API you expose.
[00:07:50]
Right.
[00:07:51]
So you still need some validation to ensure that the integer is within acceptable bounds.
[00:07:58]
So it needs to be between zero and two billion, which is the upper limit for integers, for
[00:08:03]
instance.
[00:08:04]
So if you go past that, you still need to validate within the positive integer module
[00:08:10]
that is correct.
[00:08:12]
Interesting.
[00:08:13]
Yeah, I mean, that that's something that you kind of need to look at the particular needs
[00:08:20]
of your domain and figure out the way to model that.
[00:08:23]
And, you know, there's there's no one solution.
[00:08:26]
It's really an exercise in thinking about your constraints and thinking about how the
[00:08:30]
API you expose enables those constraints and rules out anything else.
[00:08:36]
Yeah, exactly.
[00:08:38]
So another example I think of sometimes is money.
[00:08:41]
You know, let's say you've got a dollar amount and maybe you're representing it in cents.
[00:08:47]
So I've actually run into this before where you have a dollar amount.
[00:08:55]
And if you expose the constructor, maybe maybe you really want to make sure that that money
[00:08:59]
is non negative, right?
[00:09:03]
Or maybe if you're representing that money in a negative way, maybe the internals of
[00:09:08]
how you represent that money might change over time.
[00:09:11]
So maybe you could represent it as a negative int, or you could represent that with a particular
[00:09:18]
value that says whether the money is negative or not.
[00:09:22]
And those API design decisions, those things evolve over time as your API evolves, as your
[00:09:29]
domain evolves and the problems you're trying to solve.
[00:09:32]
And so I think this leads us to another point about opaque types, which is that it allows
[00:09:39]
you to hide the implementation details and make it so that users aren't depending on
[00:09:46]
those implementation details.
[00:09:48]
And so downstream, you don't make breaking changes because you've hidden the internals
[00:09:52]
and so you can freely change them as long as the public API remains the same.
[00:09:57]
Yes, that's something that I noticed a lot of when writing applications is that you start
[00:10:03]
with the type, which is a record type, an alias to record type.
[00:10:08]
And then because the internals are exposed to the other modules, the other modules start
[00:10:14]
depending on them.
[00:10:18]
And then when you want to make changes to the API, to the internal structure, then you
[00:10:24]
get into troubles because lots of other modules depend on how that works.
[00:10:29]
And you need to, even for simple changes like adding a new field to the record, that's very
[00:10:37]
tedious work.
[00:10:39]
Whereas if it was a pig to start with, it would be very easy.
[00:10:44]
You only have to change it in one place, maybe also in tests and that's it.
[00:10:49]
Well, probably not even in tests.
[00:10:52]
Right, right.
[00:10:53]
Because the tests also depend on the public API that you've exposed.
[00:10:56]
Exactly.
[00:10:57]
And that's also a good point.
[00:11:00]
People often ask the question, should I test my internals with the pig types?
[00:11:04]
You can't.
[00:11:05]
It's a problem solved.
[00:11:07]
Right.
[00:11:08]
Now, okay, so this leads to perhaps a more advanced topic, but I just want to put it
[00:11:14]
out there to make people aware of this.
[00:11:19]
There are some cases, so I think of these things, you can have a module opaque value
[00:11:27]
or a module opaque type, or you can have a package opaque type.
[00:11:30]
And when I say package opaque type, I'm talking about having an Elm package that you're building
[00:11:37]
and that Elm package can have a type that's internal to the package, meaning anything,
[00:11:44]
any code within that package can use that type and see the type and anything outside
[00:11:50]
of the package cannot use the internals of that type.
[00:11:54]
So there are a lot of packages that use this technique.
[00:11:56]
For example, in Elm UI, the element type is internal.
[00:12:01]
It's got some data.
[00:12:03]
It's not just a record.
[00:12:04]
If it was a record, then you wouldn't be able to hide the details of that constructor from
[00:12:09]
the outside world.
[00:12:10]
But it's some internal type that's got whatever data that custom type has.
[00:12:15]
Maybe it's got variants you don't care or know as a consumer of that type of that package.
[00:12:22]
And all of the code in that package is able to use those internal details.
[00:12:28]
And it's protecting those constraints and those validations to anybody who's using the
[00:12:35]
package.
[00:12:36]
But it can be used anywhere.
[00:12:37]
Those internals can be directly used anywhere within that package.
[00:12:41]
Yes, because that package still enforces all the constraints on a package level.
[00:12:48]
Sometimes you use a pick types on the module level to ensure guarantees inside it.
[00:12:54]
But when you have a package and that is well built, you don't need to have them inside
[00:13:01]
the package.
[00:13:02]
You can just enforce them at the whole level, the package level.
[00:13:05]
Exactly.
[00:13:06]
Exactly.
[00:13:07]
So that could really be a whole topic for an episode.
[00:13:11]
But just to get people aware of that pattern, if you're browsing through the documentation
[00:13:16]
in an Elm package, and you see type alias element equals element, you're like, what?
[00:13:23]
What's that?
[00:13:25]
It's some voodoo magic that is using this package opaque type.
[00:13:31]
So should we talk a little bit about how to organize code in opaque modules and what that
[00:13:35]
looks like?
[00:13:36]
Yes, let's do that.
[00:13:38]
Okay, so this is this is a really interesting topic.
[00:13:42]
Sometimes I see people organizing their code with update helpers, view helpers.
[00:13:49]
They have like a module that has their types defined.
[00:13:55]
And I think there are times, there are plenty of times when having a module that just has
[00:14:01]
some view logic is a great idea.
[00:14:04]
There's sometimes when you might want to have some utility helpers, maybe some update function
[00:14:08]
helpers.
[00:14:09]
But I think that just as a general rule, organizing your modules according to this is view related
[00:14:18]
stuff, this is update related stuff, you miss out on some of these opportunities of constraints
[00:14:24]
that you can use opaque types to help you enforce.
[00:14:28]
So for example, let's say that we had a money module, and we define the money type in one
[00:14:37]
module and we define something that shows you how to display money in another module.
[00:14:43]
Well, okay, let's say we start off with a representation of money is dollars and cents.
[00:14:52]
And we move that over to representing money as just cents, the lowest denomination.
[00:14:58]
Now since since we had a types module that had our money type, as well as several other
[00:15:04]
types for our application, that module can't be opaque because the way to present money,
[00:15:10]
the function that gives you a string form of the money, the function that gives you
[00:15:15]
an HTML form of the money, the function that allows you to add together to money values,
[00:15:22]
those things all live in separate modules.
[00:15:24]
And because they live in separate modules, you can't make it opaque, because the only
[00:15:28]
way to do an opaque type is if the type lives with the functions that deal with that type.
[00:15:35]
And so that's a general pattern that there's a type at the center of an opaque module or
[00:15:41]
just a module in general.
[00:15:43]
Now that doesn't mean that there can't be modules that just have helpers that are here
[00:15:48]
are some view helpers.
[00:15:49]
That's that's reasonable, but just be aware of, of that.
[00:15:55]
Trade off.
[00:15:56]
Yeah, beyond the lookout for when am I spreading the type and the things that deal with this
[00:16:02]
type in different places, I'd say that that is a code smell.
[00:16:05]
Yeah, potentially you lose some guarantees.
[00:16:08]
Yes.
[00:16:09]
Because if you do that, potentially you lose some guarantees because you can use the money
[00:16:15]
type in other modules, you can use, you can look at and use the internals.
[00:16:20]
And that's not what you want to have.
[00:16:22]
Exactly.
[00:16:23]
So you've now started depending on the internals of that implementation throughout your entire
[00:16:29]
code base.
[00:16:30]
And so now if that changes, you have to think about, okay, was anybody depending on these
[00:16:36]
internals in a way that I'm going to break if I change them?
[00:16:39]
Whereas if you surely surely, surely no one, if I leak details, no one will use them.
[00:16:46]
Exactly.
[00:16:47]
Exactly.
[00:16:48]
I mean, you're going to get money showing up where, you know, the you're showing a hundred
[00:16:54]
times more money than you had because somebody was presenting it in some way or sending it
[00:16:58]
off to some API depending on your internals.
[00:17:00]
And you don't want to worry about that.
[00:17:02]
So for critical parts of your domain, it's, it's even more important to use these opaque
[00:17:08]
types because you want to be careful about the internals and you don't want there to
[00:17:13]
be bugs, right?
[00:17:14]
So in cases where it's very important not to have bugs, you want to hide the internals
[00:17:19]
and an opaque type of how you do that.
[00:17:22]
Well, the thing is, it's not necessarily about hiding the internals.
[00:17:26]
Sure.
[00:17:27]
That's a good point.
[00:17:28]
But when you want to have the guarantees, it's not because you want to hide the details,
[00:17:34]
it's because you want to limit what people can do with it.
[00:17:38]
So you want an API to limit what are the operations that you can do with the type.
[00:17:44]
So for money, you could display it in some ways, in some formats, you could add it, you
[00:17:52]
could make it so you don't add money of different currencies, but it doesn't make sense to divide
[00:17:58]
money by money, for instance.
[00:18:00]
So you would not have the API allow that possibility.
[00:18:04]
And that's why what an opaque type allows you to do.
[00:18:07]
It limits what you can do with a type.
[00:18:11]
Right.
[00:18:12]
That's a great point.
[00:18:14]
Yeah.
[00:18:15]
And it also allows you to protect certain constraints about that.
[00:18:17]
So for example, I've got a blog post I wrote about this thing I use the term exit gatekeepers.
[00:18:25]
And so I like I use the example of a social security number.
[00:18:29]
So if you have a social security number, you want to be very careful with what you do with
[00:18:34]
that.
[00:18:35]
If you have a sort of logging service, if you get some sort of error log and it just
[00:18:38]
takes some data and presents it and if it has access to the internal representation
[00:18:44]
of that type, you could imagine that the error logs are going to show up with people's social
[00:18:48]
security numbers, which is not great.
[00:18:51]
Yep.
[00:18:52]
Don't do it.
[00:18:54]
Don't do it.
[00:18:55]
For their sake, don't do it.
[00:18:57]
Right.
[00:18:58]
The take module is a great way to be confident about protecting constraints.
[00:19:05]
And in cases of security, you want to be confident about protecting certain constraints.
[00:19:10]
In the case of things related to money, you want to be confident about protecting certain
[00:19:15]
constraints.
[00:19:17]
So I wanted to touch on one point, which is I think that sometimes it makes people uncomfortable
[00:19:23]
to organize a module that has like, okay, how do I present this money as a string?
[00:19:32]
What's the HTML look like for this money?
[00:19:35]
How do I perform an HTTP request and encode this?
[00:19:39]
So like, so you might have a JSON decoder for money.
[00:19:42]
You might have a JSON encoder for money.
[00:19:45]
You might have a function that returns HTML or that returns a string.
[00:19:50]
In addition to just the functions that add them together.
[00:19:53]
I think that that can make some people uncomfortable because it feels like that's a different concern.
[00:19:58]
Like that's, that's how you display money.
[00:20:02]
That's how you turn it into HTML.
[00:20:04]
The thing is, I find that when I'm trying to understand what's going on with the code,
[00:20:09]
I often like to have it organized in that way.
[00:20:12]
It's very natural to say something's off with how this money is being formatted.
[00:20:18]
Where do I look for that?
[00:20:20]
The money module.
[00:20:22]
It's very natural, but it also has this added benefit that you can enforce all the constraints
[00:20:27]
in this one place where you have access to the internals and you have the responsibility
[00:20:33]
to enforce those constraints.
[00:20:35]
And that responsibility doesn't leak outside of that module.
[00:20:38]
So if anything is wrong with the constraints being enforced or the, or the way that that's
[00:20:42]
being presented, you know where to look.
[00:20:46]
I don't know if I would put the view inside the money module.
[00:20:50]
The reason why I would probably do that is if the view needs to contain something that
[00:20:55]
I do not want to be used somewhere else, then I would probably do that.
[00:21:01]
Like for instance, if I, if there was some, if there were some internal details that I
[00:21:08]
needed to show, but that would be dangerous to leak out, then I would make the view be
[00:21:14]
inside the money module.
[00:21:16]
Otherwise I think it's fine to have it outside the money module.
[00:21:20]
Right.
[00:21:21]
You're saying that you would define the two string function for the money inside the money
[00:21:25]
module and then anything that wanted to present the HTML would call the two string.
[00:21:30]
Exactly.
[00:21:31]
What if you have some special way to display it where you know you display like, I don't
[00:21:37]
know, you have some special HTML class that is applied.
[00:21:40]
So if it's negative, then you show it in red with parentheses around it, or if it's positive,
[00:21:47]
you show it in green.
[00:21:48]
And if it's zero, you show it in gray.
[00:21:53]
I guess you could create a new custom type.
[00:21:56]
Yeah.
[00:21:57]
Yeah, that's a good, that's a good way to approach that problem.
[00:22:00]
I guess the, the general sensibility is what you have to develop, which is I'm trying to
[00:22:07]
protect these constraints.
[00:22:08]
I don't want to leak these constraints outside of the module.
[00:22:11]
And so yeah, as you say, you can create a new custom type, which is the public interface
[00:22:17]
that describes is this, you know, maybe it's a custom type with three variants.
[00:22:24]
So it's money that's either positive, zero or negative.
[00:22:29]
And you ask for money and it gives you this custom type.
[00:22:32]
And then you have another module, or maybe you can depend on those details of those variants
[00:22:39]
of that custom type in directly in your view function or wherever it's appropriate.
[00:22:45]
Yeah.
[00:22:46]
But I guess the general thought process is what constraints do I want to protect?
[00:22:53]
What are the internals that I want to use to enforce those constraints and that I want
[00:22:57]
to protect the outside world from depending on?
[00:23:00]
Yes, exactly.
[00:23:02]
So as long as you're doing that, that's what leads you in the right direction.
[00:23:06]
And it may take many different forms, but I do find that often the things that belong
[00:23:11]
in a particular module might be more than what somebody would expect.
[00:23:16]
So for example, how do you make an HTTP request to, you know, maybe you need to fetch an auth
[00:23:22]
token and you've got, you know, you might think like, well, I've got my HTTP requests
[00:23:28]
module and this performs all my HTTP requests.
[00:23:31]
So here's how I do an HTTP, like here are all the HTTP requests that I do to my server
[00:23:39]
and I store it in this module or I store it in the module for this page or something.
[00:23:45]
Yeah.
[00:23:46]
Well, in a way the details about how you perform the authentication request and how you decode
[00:23:52]
that value, they kind of belong with that authentication token type.
[00:23:58]
So there's this type, it's opaque.
[00:24:01]
You don't want somebody to be able to just construct an auth token with a string to just
[00:24:06]
fake it and pass in an auth token.
[00:24:08]
Like you're either authenticated or you're not.
[00:24:10]
If you're not authenticated, just be honest with me, you're not authenticated and I'll
[00:24:15]
let you access what you can, not being authenticated, but don't just give me an empty string.
[00:24:21]
And so the only way you can get an auth token type is by performing an HTTP request, which
[00:24:27]
is exposed from that auth token module.
[00:24:30]
Yeah.
[00:24:31]
And maybe that one needs to be configured, but just the function that will make that
[00:24:36]
HTTP request will take the needed parameters.
[00:24:39]
Right.
[00:24:40]
So for example, perhaps you inject the dependency from the outside of what is the base URL for
[00:24:46]
the API.
[00:24:47]
So if it's like a staging server or a dev server or a production server, you inject
[00:24:54]
that.
[00:24:55]
So those details are not coupled to your auth token module.
[00:25:00]
So I think that this is, I just want people to be aware that it's okay to have these different
[00:25:06]
types within a module kind of living together.
[00:25:10]
It can actually work quite elegantly and just do what it takes to protect your constraints.
[00:25:16]
That's the first and foremost thing.
[00:25:19]
And nice design will follow from protecting those constraints.
[00:25:22]
Yeah.
[00:25:23]
I think that's a question always with when you write an Elm type is what constraints
[00:25:28]
do I want?
[00:25:30]
So when you try to, when you start to leak out details, what guarantees do you lose?
[00:25:36]
Exactly.
[00:25:37]
So I think that, I think that if somebody wants to get started applying this technique
[00:25:42]
of using opaque types, I think that the very first thing to start doing is to start with
[00:25:49]
that mindset of thinking about the constraints and actually stepping back from that even,
[00:25:56]
what is the problem you want to solve?
[00:25:58]
Exactly.
[00:25:59]
And how could a type or an API and hiding some of those types in an opaque module, in
[00:26:04]
an opaque type, how would that help you enforce those constraints or solve that problem?
[00:26:09]
Yeah.
[00:26:10]
So any parting wisdom on how people can get started using opaque types?
[00:26:15]
Yeah.
[00:26:16]
I've got one very good piece of advice.
[00:26:21]
Put your type into a separate module and make it opaque.
[00:26:26]
There you go.
[00:26:28]
Just do it.
[00:26:29]
And you can, you can take an existing module.
[00:26:31]
Let's say that you've taken, you've had a money module that exposed the constructor.
[00:26:39]
Well the good news is with Elm, I don't know if you know this, but if you start breaking
[00:26:45]
things, then the compiler will tell you what you broke.
[00:26:47]
Is that true?
[00:26:48]
Yeah.
[00:26:49]
It's a pretty cool feature.
[00:26:51]
It's like a linter or something.
[00:26:52]
It's really powerful.
[00:26:53]
Oh, nice.
[00:26:54]
So if you want to get started, a good first step is to take a type, maybe it's like a
[00:27:04]
business critical type, like money or something that's core to your domain.
[00:27:09]
Maybe it's something you've been having bugs in this area.
[00:27:13]
I had an example in Elm GraphQL in the generator code for the CLI where I generate the Elm
[00:27:20]
GraphQL code.
[00:27:21]
I had some bugs that I, bugs kept coming up around the way I was normalizing values where
[00:27:30]
in some cases I need to normalize something to be a valid Elm name so I can have an Elm
[00:27:36]
function that's based on some value from a GraphQL schema.
[00:27:42]
So sometimes I need to normalize it, but sometimes I need to use the actual name as far as the
[00:27:47]
GraphQL schema is concerned.
[00:27:49]
For example, if I'm performing the request, I need to use the name GraphQL knows, the
[00:27:53]
unnormalized name.
[00:27:55]
If I'm generating a function or a type, I need to normalize it according to certain
[00:27:59]
rules.
[00:28:00]
So I had bugs coming up in this area a few times and I'm like, you know what?
[00:28:06]
I need to use the type system and module design to protect these constraints.
[00:28:12]
Because in both cases you were using strings, I'm guessing.
[00:28:16]
Exactly.
[00:28:17]
The strings were getting passed down.
[00:28:18]
Exactly, so I just wrapped it in a custom type.
[00:28:21]
I made that custom type hidden in a module so it's a private constructor so it's an opaque
[00:28:26]
type.
[00:28:28]
You know, I think I called it like a camel cased name.
[00:28:31]
So then I know this represents a camel cased name and I can get, I had basically a getter
[00:28:39]
that allows me to either get the raw unnormalized value or a valid Elm identifier.
[00:28:47]
And then what happened is I, so this is a trick that I like to use is to, you want to
[00:28:55]
hold on to the type that represents like the purest form and the constraints as long as
[00:29:02]
possible.
[00:29:03]
Yeah.
[00:29:04]
So I think of this as wrap early, unwrap late.
[00:29:08]
I agree.
[00:29:09]
So as soon as something comes in, you want to wrap the type in this type that represents
[00:29:17]
something or as we kind of talked about, in some cases it may perform a validation and
[00:29:21]
it may conditionally return that if that validation succeeds.
[00:29:27]
Or in some cases like an auth token, you have an HTTP request and the only way to get that
[00:29:34]
auth token type is through the HTTP request function that's exposed in that module.
[00:29:41]
And so you want at the earliest possible moment, you want something to have this type which
[00:29:46]
represents the constraints about it.
[00:29:49]
And then at the latest possible moment, you want to turn that into a primitive or a side
[00:29:54]
effect or whatever it may be.
[00:29:56]
So in the case of a normalized or unnormalized name in my Elm GraphQL CLI, at the very last
[00:30:04]
possible moment, when I turn something into a GraphQL request or when I turn something
[00:30:09]
into a function name that I'm going to generate in the generated code, that's the moment that
[00:30:14]
I do it and no sooner because otherwise I'm just passing strings around.
[00:30:17]
I don't know what they represent.
[00:30:18]
I can't use it to enforce those constraints and those semantics.
[00:30:22]
And you got the same problem over again.
[00:30:24]
Exactly.
[00:30:25]
So now that brings up another thing which is that opaque types are a great way to enforce
[00:30:30]
semantics.
[00:30:31]
Just passing something around, you know what it represents.
[00:30:34]
And as you said before, you know what the valid operations are to do with it.
[00:30:38]
So the API design, that's the only thing that allows you to protect what can I do with this
[00:30:43]
type of thing.
[00:30:45]
Because often what people do is they make a type alias.
[00:30:47]
Like type alias, user equals some records with a lot of details.
[00:30:53]
So the semantics are there, but you have no guarantees.
[00:30:57]
Exactly.
[00:30:58]
Exactly.
[00:30:59]
Okay.
[00:31:00]
So if you want to get started with opaque types, then pick a module, pick something
[00:31:06]
that's been causing problems, creating bugs, or just been difficult to maintain.
[00:31:12]
Maybe you have to make breaking changes often because some of your code downstream that
[00:31:16]
depends on it is directly using internals.
[00:31:20]
And then you can just try one small step.
[00:31:24]
You can just make that constructor private, see what happens, see what breaks, and then
[00:31:29]
try changing those areas that break.
[00:31:32]
And then in fact, you can even expose the constructor directly as a function.
[00:31:39]
At least to start with.
[00:31:40]
At least to start with.
[00:31:41]
Exactly.
[00:31:42]
That's one small step that gets you in the right direction and then start playing around
[00:31:45]
with it and massaging in those constraints.
[00:31:47]
Yeah.
[00:31:48]
And while you do that, you will notice you will need access to certain details of the
[00:31:53]
type and some of them may look odd.
[00:31:55]
Like why do you need that information at that location?
[00:31:59]
And maybe you'll notice some smells with that.
[00:32:03]
Right.
[00:32:04]
Right.
[00:32:05]
So yeah, get good at noticing those smells.
[00:32:08]
Get good at noticing when you're depending on internals, when you're using a type that's
[00:32:13]
defined in one module, using functions defined in another module.
[00:32:18]
Be on the lookout for those types of smells.
[00:32:20]
Be on the lookout for primitive obsession, having these primitives that you're depending
[00:32:24]
on everywhere and you lose track of the semantics and the valid operations.
[00:32:28]
So you're performing operations on them that shouldn't be valid, but they're just a primitive,
[00:32:34]
so there's nothing keeping you from doing that.
[00:32:36]
Yeah.
[00:32:37]
I'd love to learn more about that technique.
[00:32:39]
Just get used to doing it myself.
[00:32:41]
Yeah.
[00:32:42]
Yeah.
[00:32:43]
I actually gave a talk at a conference last year that I showed this technique for, I called
[00:32:50]
it this technique of wrapping something in a semantic type.
[00:32:54]
And it's actually quite cool because you can wrap something in a type, kind of like I described
[00:32:59]
for this type that represents a value that represents a name in a GraphQL schema.
[00:33:07]
You wrap it in that type and then instead of having primitives, a string that you're
[00:33:12]
passing to this function, passing to this function, then returning in this function,
[00:33:18]
you just say, oh, actually this is no longer a string.
[00:33:21]
This is this opaque type.
[00:33:24]
And then you change all these type signatures everywhere.
[00:33:26]
Instead of saying it takes a string and it returns this, you say it takes this opaque
[00:33:30]
type and then it returns this.
[00:33:33]
And you keep doing that all the way until you get to the very end.
[00:33:35]
And then at the very end, you call some function that takes that opaque type and gives you
[00:33:40]
a primitive.
[00:33:42]
And all the way through, all you've done is change some type annotations so that it says,
[00:33:47]
instead of passing a primitive, you're passing an opaque type.
[00:33:50]
And then at the very end, at the very edge, you return that primitive at the last possible
[00:33:55]
moment.
[00:33:56]
You wrap early and you unwrap late.
[00:33:58]
It's very easy.
[00:33:59]
It's a very safe refactoring.
[00:34:01]
And it's a great way to have nice semantics that ensure that you're using things with
[00:34:07]
the appropriate actions.
[00:34:08]
Yeah.
[00:34:09]
It's very safe.
[00:34:10]
Yes.
[00:34:11]
Because you only need some wiring.
[00:34:12]
You don't change business logic.
[00:34:15]
Exactly.
[00:34:16]
I mean, really it's the kind of thing that IDE tooling would be really good at because
[00:34:20]
it's a very tedious, mindless wiring exercise.
[00:34:25]
But it's powerful because it opens up these constraints that you can model through the
[00:34:29]
API design.
[00:34:30]
Yeah.
[00:34:31]
So one other way of starting with opaque types is to create a new module, new opaque type.
[00:34:37]
I think we would both advise people to start making the types opaque by default.
[00:34:43]
Right?
[00:34:45]
It's a really interesting question and it's one I've thought about.
[00:34:51]
I think at the very least, play around with it, try it, see how it feels.
[00:34:55]
Try making some things opaque by default.
[00:34:58]
You know, I mean, there are times when using a record is appropriate, right?
[00:35:03]
I mean, can your model in your application be a record?
[00:35:07]
Yeah, that's absolutely fine.
[00:35:09]
That's a great representation for that, for example.
[00:35:11]
So just I would say that try that.
[00:35:16]
Try making something opaque as a starting point.
[00:35:18]
Try creating a new module for something you're trying to represent.
[00:35:23]
I think that it does take some judgment and I think that you, because if you try to make
[00:35:28]
everything an opaque type, it does introduce an overhead.
[00:35:33]
Obviously.
[00:35:34]
And I think that for you saying just use an opaque type right away, I think that you have
[00:35:40]
the judgment and the experience to know this is probably going to be a good idea.
[00:35:45]
So it feels natural.
[00:35:47]
But I think that if somebody who's new to opaque modules were to try to do that, they
[00:35:51]
might say, oh, OK, well, here's my model for my application.
[00:35:55]
So I'll put that in a module and I'll make that an opaque type or I'll make that a custom
[00:35:59]
type.
[00:36:00]
Yeah, right.
[00:36:01]
Well, there are two reasons why I would do it.
[00:36:04]
Is one, it's much easier to turn an opaque type into a non opaque type than the other
[00:36:09]
way around.
[00:36:10]
Right.
[00:36:11]
So if you have a type and you need to make it opaque, that will be tedious.
[00:36:17]
So as you said, an IDE would be great for that, but we don't have any IDE that does
[00:36:23]
that.
[00:36:24]
Not yet.
[00:36:25]
Crossing my fingers.
[00:36:26]
Crossing my fingers.
[00:36:27]
Me too.
[00:36:28]
Yeah.
[00:36:30]
So I think that what I would recommend is that push yourself out of your comfort zone
[00:36:37]
a little bit.
[00:36:38]
So if it feels like a stretch to start with an opaque module, maybe don't do it all the
[00:36:45]
time, always, but try doing it for a day and see how that goes and see what you learn.
[00:36:52]
When was it not working well and why was that not working well and when did it work surprisingly
[00:36:58]
well?
[00:36:59]
You can't learn something new unless you push yourself to try a different way of working.
[00:37:03]
And with opaque modules, it's a different mindset.
[00:37:06]
And so you have to push yourself to try something new and think of it as an experiment, see
[00:37:11]
how it goes.
[00:37:13]
Film is great for refactoring and changing things and telling you where you need to fix
[00:37:16]
things.
[00:37:17]
So just try something.
[00:37:18]
And then above all, experiment to try new ways of working with that mindset of what
[00:37:25]
problem am I trying to solve and how can types help me do that?
[00:37:29]
How can I enforce constraints using types and module design, API design?
[00:37:35]
Yeah.
[00:37:36]
Basically, if you don't gain any guarantees from using a big type, don't use it.
[00:37:41]
That's my baseline.
[00:37:42]
But as you said, you probably need some experience with that.
[00:37:45]
So try making it opaque to start with.
[00:37:49]
Don't mind the boilerplate.
[00:37:50]
When you're used to boilerplates in Elm, it feels all for us first.
[00:37:56]
It's not that bad.
[00:37:57]
Yes.
[00:37:58]
And when you use boilerplates, it's usually for something good like getting new guarantees.
[00:38:04]
Exactly.
[00:38:05]
And one micro tip, when you're doing this technique, I think a lot of people don't know
[00:38:12]
this trick that if you have a custom type with a single variant, you can destructure
[00:38:20]
that custom type variant directly in your function signature.
[00:38:25]
So if I have type money equals money int where int is the sense.
[00:38:31]
In the money module, I can have a two string function that takes money and returns a string.
[00:38:36]
So I could say two string and then in parentheses, I can do capital money.
[00:38:42]
So that's destructuring my money just like I would do in a case statement.
[00:38:46]
But it's not in a case statement.
[00:38:48]
It's two string parentheses money.
[00:38:51]
And then I destructure that int in some variable name money value.
[00:38:57]
And now I don't have to do a case statement.
[00:38:59]
It's just one little trick that will make your life a little bit easier.
[00:39:02]
Yeah, I think we're all very happy when we learn that technique.
[00:39:06]
Yeah, absolutely.
[00:39:07]
Yeah, this single variant custom type ends up happening a lot when you're using opaque
[00:39:13]
types.
[00:39:14]
Yeah, it removes like half of the boilerplate.
[00:39:16]
Yeah, exactly.
[00:39:18]
All right.
[00:39:19]
Well, I think there's a lot more to discuss in the episode, but hopefully this is a good
[00:39:24]
starting point.
[00:39:25]
Yeah, we hope you understand the big types now and that you won't be lost when we talk
[00:39:29]
about them in future episodes.
[00:39:31]
Yeah, hopefully if people just give these a try, take them for a spin and start taking
[00:39:37]
a step in this direction of experimenting, then that's a success.
[00:39:40]
So take them for a spin.
[00:39:41]
See what you think.
[00:39:42]
Yeah.
[00:39:43]
All right, Jeroen.
[00:39:44]
Well, I'll talk to you next time.
[00:39:45]
Have a good day.
[00:39:46]
Have a good day.