spotifyovercastrssapple-podcasts

Parse, Don't Validate

We discuss the Alexis King's article and how those techniques apply in Elm.
August 24, 2020
#11

the difference between validation and parsing lies almost entirely in how information is preserved

Shotgun parsing is a programming antipattern whereby parsing and input-validating code is mixed with and spread across processing code—throwing a cloud of checks at the input, and hoping, without any systematic justification, that one or another would catch all the “bad” cases.

Why the term "parse"?

a parser is just a function that consumes less-structured input and produces more-structured output [...] some values in the domain do not correspond to any value in the range—so all parsers must have some notion of failure

  • Conditionally return types
  • Don't have to repeatedly check condition
  • Look out for "lowest common denominator" built-in values being passed around (like empty String)
  • Maybe.withDefault might indicate an opportunity to parse

Two ways to use this technique:

Transcript

[00:00:00]
Hello, Jeroen.
[00:00:01]
Hello, Dillon.
[00:00:02]
How are you doing today?
[00:00:03]
I'm doing pretty well.
[00:00:04]
How about you?
[00:00:05]
I can't complain.
[00:00:06]
I'm thinking about this concept of parsing, but not Elm parsers.
[00:00:14]
Parsing versus validating.
[00:00:15]
I.e.
[00:00:16]
parse don't validate.
[00:00:17]
I've heard that article before.
[00:00:20]
Yeah, this is kind of fun.
[00:00:22]
It's actually, it kind of feels like a book club a little bit.
[00:00:25]
I reread this article which is sort of circulated around.
[00:00:28]
It's by Alexis King, Lexi Lambda.
[00:00:31]
It really encapsulates a lot of really nice ideas and expresses them very well.
[00:00:35]
Yeah, yeah, definitely.
[00:00:37]
So can you give a summary of what it is about?
[00:00:40]
It was nice rereading this article because I sort of tried to capture a couple of quotes
[00:00:44]
that I think nicely summarized the article itself.
[00:00:50]
And this one really stuck out to me.
[00:00:53]
The difference between validation and parsing lies almost entirely in how information is
[00:00:58]
preserved.
[00:01:00]
So I think that it's really keeping track of information.
[00:01:05]
As you go deeper into the logic and function calls in your code, you should have more and
[00:01:11]
more highly structured data.
[00:01:13]
You should have more things that you've proven through the data types that you have.
[00:01:18]
A perfect example is JSON, right?
[00:01:21]
At the periphery of your application, maybe like through like a flag or a port, you could
[00:01:26]
have some JSON data coming through.
[00:01:29]
But in the core of your Elm application, you're going to get that in a structured way.
[00:01:34]
I think it's important to point out what this technique or this technique tries to prevent
[00:01:41]
you from doing, which pitfalls it prevents you from falling into.
[00:01:46]
And that is to check things several times.
[00:01:50]
Yes, exactly.
[00:01:52]
What was the, I think there was like an academic article that she was referencing referring
[00:01:58]
to shotgun parsing.
[00:02:00]
Yeah, shotgun parsing.
[00:02:01]
So the idea is that you check for something once, like, is this list empty?
[00:02:07]
In one condition, you do something and the other condition you say, hey, we don't want
[00:02:11]
to have an empty list in this case.
[00:02:14]
And then in the condition where you said, where you notice that the list is not empty,
[00:02:18]
you do some other operations.
[00:02:20]
And then you have to check because of some other operation that the list is empty or
[00:02:26]
not again.
[00:02:28]
And if you did it with a if then else statement, then you lost that information and you have
[00:02:34]
to make the check again, meaning that you have to handle the case where the list is
[00:02:38]
empty.
[00:02:39]
Exactly.
[00:02:41]
You haven't preserved that information that you had.
[00:02:44]
You had the information and then you lost it.
[00:02:48]
So the type system can no longer help you with that.
[00:02:50]
Yeah.
[00:02:51]
So you have to handle the case again, even though you knew it already.
[00:02:56]
Or keep track of it in your head and say, oh, at this stage, I know that this data has
[00:03:02]
already checked for this.
[00:03:03]
Or I think I know.
[00:03:05]
Exactly.
[00:03:06]
I'll check later if I did check that again.
[00:03:10]
See also JavaScript.
[00:03:11]
Come on, don't throw JavaScript under the bus.
[00:03:17]
I mean, we touched on this in our in our JSON decoding episode a little bit that this is
[00:03:23]
a very familiar experience.
[00:03:25]
And you can certainly write JavaScript in a way where you're handling the validation
[00:03:30]
step up front as sort of a single step rather than this sort of shotgun parsing approach.
[00:03:38]
But I do find that it's quite common that you see this in JavaScript where you're sort
[00:03:42]
of mixing processing data with validating the data.
[00:03:46]
Yeah, so can you explain what shotgun parsing is?
[00:03:48]
Because I don't think we did.
[00:03:50]
We explained it.
[00:03:51]
Yes, I think shotgun parsing is just defined as mixing processing data with validating
[00:03:58]
data.
[00:03:59]
So it's your intermingling those processes rather than sort of validating data upfront
[00:04:04]
and parsing it in a single process.
[00:04:07]
And then now that you have the data, process that data.
[00:04:10]
Does that seem like a fair definition?
[00:04:13]
Let's just reread the article real quick.
[00:04:16]
So here's a quote.
[00:04:17]
Shotgun parsing is a programming anti pattern, whereby parsing and input validating code
[00:04:22]
is mixed and spread across processing code, throwing a cloud of checks at the input and
[00:04:28]
hoping without any systematic justification that one or another would catch all the bad
[00:04:34]
cases.
[00:04:35]
Yeah, so for me, it means that you have if else statements all over the place.
[00:04:41]
And then you hopefully have enough to make sure that all the cases are handled.
[00:04:47]
And maybe there are maybe they're not.
[00:04:50]
I was recently working with some some Ruby code.
[00:04:54]
And there's like a, you know, in the controller, the Ruby controller, MVC controller, you can
[00:05:00]
you can say like params, and then get whatever, you know, data that's been passed in in the
[00:05:07]
in the request.
[00:05:09]
And it's quite common to see Ruby code where you just sort of grab the params and maybe
[00:05:15]
like you you try to remember to check in front of the code, maybe you add like you can add
[00:05:21]
these like before filters and that sort of thing in in your rails controllers where you're
[00:05:27]
you're asserting certain things about the data before you get it.
[00:05:30]
But then you're just directly reading like params sub username or something and
[00:05:37]
params dot sub dot username, you mean something like that?
[00:05:41]
Well, like params square brackets, some value.
[00:05:44]
So you're like grabbing a key out of hash that's just grabbing this raw data.
[00:05:49]
And it seems very prone to this sort of shotgun parsing anti pattern.
[00:05:55]
So another thing that I've seen, I was working with with some Ruby code recently that I found
[00:06:00]
to be a nice pattern that there was this sort of validator class that you could sort of
[00:06:06]
assert things about the data, and then you pass that data after you've made the assertions.
[00:06:12]
So you're never directly accessing the params in your controller, you're grabbing data after
[00:06:18]
you've made these validations.
[00:06:19]
So you can certainly like use this pattern of thinking parse don't validate in non typed
[00:06:25]
languages.
[00:06:26]
But when you're working with the typed language, you can actually have your type signatures,
[00:06:31]
keep you honest to make sure that your data types have the proofs in them.
[00:06:36]
So you could say this has to be a non empty list.
[00:06:39]
And now you can't just forget like, oh, I need to be sure to check all these invariants
[00:06:45]
at the top, you've proven by the time you call that code because it requires a non empty
[00:06:50]
list type.
[00:06:52]
Let's talk a little bit about the term parse, because that's certainly something that stuck
[00:06:57]
out to me when I when I first read this article.
[00:07:00]
You know, it's a little confusing.
[00:07:01]
And she does kind of get into that.
[00:07:03]
And she says, let me try to convince you that this word parse is actually a good term for
[00:07:07]
this concept.
[00:07:08]
It might seem unrelated at first.
[00:07:11]
So she's sort of saying, what is a parser?
[00:07:14]
It's a function.
[00:07:16]
So she's here's a quote, a parser is just a function and produces more structured output.
[00:07:21]
Some values in the domain do not correspond to any value in the range.
[00:07:25]
So all parsers must have some notion of failure.
[00:07:28]
So I think that's an important point, right is that if you're able to fail, then we kind
[00:07:34]
of talked about this in our opaque types episode that you can have a function that conditionally
[00:07:39]
returns a type.
[00:07:41]
And the way you do that in Elm is you can return a result or maybe type or a custom
[00:07:47]
type.
[00:07:48]
Yeah, right.
[00:07:49]
And that represents a failure.
[00:07:50]
So you have to handle the failure step.
[00:07:52]
But once you check for failure, so you say, you know, case parse result, okay, error.
[00:07:59]
Well in the okay case, you know, you have that data, you know, you've validated whatever
[00:08:04]
you validated by the data that it's a non empty list that it's an authenticated user
[00:08:08]
that it's a valid username, whatever you've validated, you can conditionally return a
[00:08:13]
type and you know that the only way you can get that type is by running it through a function
[00:08:19]
that will conditionally give you that type if the validation passes.
[00:08:22]
So it's sort of this technique of just instead of just doing a conditional, and then losing
[00:08:28]
the proof that you've done in that conditional, you return a type, if it's true, and else
[00:08:35]
you return nothing or you return an error.
[00:08:38]
And now you're able to keep track of that proof that you've just done.
[00:08:43]
Yeah, the simplest example that I can find is when you do case of expression on a maybe.
[00:08:50]
So you do case, maybe something of, and then you have two cases, just something, and then
[00:09:00]
you have access to that something as if it is not a maybe, and then nothing where you
[00:09:06]
don't have it.
[00:09:08]
And then in that case where you have something, you just use it to what like normal value.
[00:09:13]
And you have successfully branched off depending on that value, but you have remembered in
[00:09:21]
that branch that the value is there.
[00:09:24]
You don't start again with a maybe something.
[00:09:26]
Yes, exactly.
[00:09:28]
You've proven that and so now you can work with that type without having to check for
[00:09:33]
it again.
[00:09:34]
And yeah, I think that's a great example.
[00:09:37]
And what often occurs, like to contrast that with the alternative to what you just described,
[00:09:43]
you just keep passing the maybe down, even though you've sort of unwrapped it in that
[00:09:47]
case statement and you've verified that it's present, if you expect it, sometimes you'll
[00:09:52]
see things like, you know, maybe with default empty string.
[00:09:56]
Now that may be what you want in some cases, and it's not inherently bad to use maybe.withDefault.
[00:10:03]
But I think it's good to be aware that sometimes maybe.withDefault is a smell that indicates
[00:10:09]
that you're basically validating rather than parsing.
[00:10:12]
Yeah, and in some other way you lose information of what was in the maybe beforehand.
[00:10:18]
Right.
[00:10:19]
You still have access to that maybe value, but you probably won't use it anymore.
[00:10:25]
Right.
[00:10:26]
Like let's say that you only want to run a certain bit of logic if you have a username.
[00:10:32]
If you're a logged in user and you have a username, you pass around this maybe user
[00:10:37]
and then you say maybe.map username.
[00:10:41]
And maybe you pass in maybe.withDefault empty string.
[00:10:46]
Now you've got a string.
[00:10:49]
You've got a string.
[00:10:50]
What does the string represent?
[00:10:52]
Well, it might represent a username or it might be an empty string.
[00:10:56]
Why is the string empty?
[00:10:57]
Is the string empty?
[00:10:59]
Does it represent a pending username that hasn't been saved or does it represent a username
[00:11:04]
that came from a user or does it represent a guest user who's not logged in and therefore
[00:11:11]
you used maybe.withDefault empty string?
[00:11:14]
It could possibly represent all those things.
[00:11:17]
And well, that's, Alexis had the phrase in her article at one point, it's a bug waiting
[00:11:24]
to happen.
[00:11:25]
That phrase often pops into my mind actually as well.
[00:11:29]
And I would call that a bug waiting to happen.
[00:11:31]
It's a time bomb.
[00:11:32]
So what would you do instead?
[00:11:34]
Well, I mean, there are a lot of ways you can handle that, but you could have, for example,
[00:11:38]
a custom type that keeps track of that information.
[00:11:41]
And so you know if you have a username, if you have a logged in user, if you try to get
[00:11:46]
the logged in user and you get nothing, check that in one spot and pass that data through,
[00:11:51]
or maybe you wrap that in a custom type and you turn that into a custom type that's either
[00:11:57]
a guest or a logged in user.
[00:11:59]
But whatever it is, you want to preserve that information.
[00:12:03]
So that's like a common thing.
[00:12:06]
If you're sort of getting the lowest common denominator data type, you're getting some
[00:12:11]
like built in data type like a string and you're passing it around and it could represent
[00:12:15]
different things.
[00:12:16]
It's a bug waiting to happen and it's probably an opportunity to use this technique of parse
[00:12:20]
don't validate.
[00:12:21]
Yeah, usually when you simplify a complex data structure, data to a single primitive
[00:12:28]
or way simpler version of it, you're bound to lose data.
[00:12:34]
And if you do need that piece of data later on, then yeah, you're going to do some assumptions
[00:12:40]
which might be correct now, but which might be incorrect later.
[00:12:44]
And what you would like to do to have is to have the compiler check things for you.
[00:12:50]
Exactly.
[00:12:51]
So if you make assumptions by yourself and do maybe with default empty string, because
[00:12:56]
it will always be true in this case, because I checked it before, then it might be true
[00:13:03]
now, but maybe later on, because you move code around and the code might not have the
[00:13:10]
check anymore beforehand, then at some point things will change and you'll have a bug basically.
[00:13:20]
Right.
[00:13:21]
Yeah.
[00:13:22]
One, one thing I like to think about is imagine somebody is new to your code base.
[00:13:27]
If they come into the code base and they start using some code, how much do you have to tell
[00:13:32]
them?
[00:13:33]
How many caveats are you going to explain to them?
[00:13:35]
Like, Oh, okay.
[00:13:37]
So like if you're doing a code review on some code, they just wrote, how afraid are you
[00:13:41]
going to be that that's going to get merged without you carefully checking that these
[00:13:45]
constraints are met?
[00:13:46]
Like, Oh, when you call this function, like if you call it with an empty string, then
[00:13:50]
then that's a problem.
[00:13:51]
So you have to make sure it's never an empty string.
[00:13:53]
Like see this code over here.
[00:13:55]
It checks if the string is empty.
[00:13:57]
Well, that burden shouldn't be on the newcomer to the code base.
[00:14:01]
That burden should be on the code that protects those conditions.
[00:14:05]
And it, you know, it needs to validate its input and then preserve that in type information.
[00:14:11]
And so it then becomes impossible because maybe you have like a non empty string type
[00:14:16]
that you take as an input to this or whatever data type that you validate into.
[00:14:20]
So one thing that I understand from this technique is that pretty much every time you want to
[00:14:26]
keep the information from the checks or the parsing that you have beforehand, you kind
[00:14:32]
of need to have a new or a specific custom type to your guarantees, to your assumptions.
[00:14:40]
So in the case of, um, I want a non empty list of something.
[00:14:45]
So yeah, you check beforehand whether your list is empty or not, you're doing a parsing.
[00:14:50]
So you get a non empty list.
[00:14:53]
So in the case of a non empty list that is quite generic, but in some cases you probably
[00:14:58]
need a new custom type.
[00:15:00]
You will have a new custom type for every step of your function calls.
[00:15:05]
And I think that's probably okay.
[00:15:08]
In practice, I don't think you will have so many new types.
[00:15:12]
That's not something that I noticed in my code base at least.
[00:15:15]
Yeah, it's not.
[00:15:16]
I mean, it's so lightweight to create types in Elm and so that's definitely not a problem.
[00:15:21]
I mean, another thing that Alexis talks about in the article is that you can use built in
[00:15:27]
types that have the characteristics that you need.
[00:15:31]
So for example, if you're saying that I can't have duplicate values in this list, then you
[00:15:37]
parse it into a set and the set data structure is going to take care of that for you.
[00:15:42]
And when you look at a set, you know that there are no duplicate keys.
[00:15:46]
You don't have to think about that.
[00:15:49]
She also talks about this technique of weakening return types and strengthening input types.
[00:15:55]
Yeah, that's nice.
[00:15:56]
That's a really nice sort of insight that she's captured there.
[00:16:00]
Those are sort of two tools at your disposal.
[00:16:02]
So we kind of talked about weakening return types with conditionally returning custom
[00:16:08]
types.
[00:16:09]
So if you want to check if you have a valid username, you can conditionally return a custom
[00:16:13]
type that represents a valid username.
[00:16:17]
And if it's not valid, then you return nothing or error or some data type that doesn't give
[00:16:22]
you the username type that represents a validated username.
[00:16:26]
Yeah, so it's a weakened compared to always returning a valid username.
[00:16:33]
Exactly so instead of returning a username, you return a maybe user or guest or user type.
[00:16:40]
Exactly exactly which JSON decoding is the same pattern you I mean, it's parsing it's
[00:16:46]
it's not parsing in the sort of computer science sense of building up an AST and parsing the
[00:16:53]
tokens that's already been done with JSON decoders.
[00:16:56]
We discussed in our JSON episode, but it is parsing in Lexi's sense of the term in Alexis
[00:17:02]
a sense of the term where you have a way for it to fail.
[00:17:06]
And if it hasn't failed, then you've guaranteed certain qualities and you capture that in
[00:17:10]
a data type.
[00:17:11]
So yeah, that's weakening the return type.
[00:17:14]
That's a very useful technique.
[00:17:15]
And then on the other side, they're strengthening the input type and those those two things
[00:17:19]
go hand in hand.
[00:17:21]
And strengthening the input type.
[00:17:23]
She gives the example of requiring a non empty list as an argument.
[00:17:27]
And I find that that sort of thing strengthening the input type, it's quite nice because you
[00:17:31]
want to write really confident code that doesn't think about validating all these conditions,
[00:17:38]
you want to demand that your callers give you that data, and then be confident about
[00:17:44]
just using it right.
[00:17:46]
And when you do that, I mean, I think that that sort of gets at that shotgun parsing
[00:17:52]
concept that you're not sort of blending these responsibilities of processing the data and
[00:17:57]
validating the data, right?
[00:17:59]
Like, you should separate those responsibilities.
[00:18:02]
So if a function's job is to process data, state what data you need upfront with the
[00:18:08]
types, the types should express the guarantees that you need.
[00:18:11]
So that's sort of that's kind of this concept of like design by contract where you're expressing
[00:18:16]
the contract, and you can work with those assumptions within that code and just confidently
[00:18:22]
work without checking those assumptions.
[00:18:24]
In the case of a typed language, and especially Elm, you can encode those guarantees and
[00:18:30]
that contract into types.
[00:18:31]
So that's strengthening the input type, be very clear about what you require as a as
[00:18:36]
a constraint.
[00:18:38]
And then just confidently work with those assumptions, rather than mixing the checking
[00:18:43]
and the processing.
[00:18:44]
Yeah, I find it kind of odd, but it actually feels very natural to people who write Elm
[00:18:52]
usually because you do this with JSON decoding all the time.
[00:18:57]
We took it as an example already.
[00:19:00]
But let's imagine if we didn't do it, what you would probably do is you get a JSON value
[00:19:05]
from an HTTP request somewhere or from flags.
[00:19:09]
And then every time you need to access one field of it, you decode it for every use every
[00:19:16]
field, but not all at once.
[00:19:19]
And that just sounds unimaginable.
[00:19:22]
Which you could do if you wanted to.
[00:19:25]
Because you can do like a JSON decode dot value.
[00:19:29]
And yeah, also, so you could defer decoding and just get a JSON decode value for a certain
[00:19:37]
subtree of your JSON, and then parse it again at a later step.
[00:19:43]
And that would be that sort of like shotgun parsing approach.
[00:19:46]
But we don't like naturally people tend not to do that.
[00:19:49]
I think maybe it's like a cultural thing.
[00:19:51]
Maybe it's like the educational material and the examples.
[00:19:54]
I think it's the education.
[00:19:57]
But it also people don't like writing JSON decoders.
[00:20:01]
So if you can do it once, that's fair.
[00:20:04]
So a lot of people say, hey, JSON decoding in Elm is a pain.
[00:20:09]
But you only do it once.
[00:20:10]
And because it's a pain, yeah, that's why you only do it once.
[00:20:14]
And that's why you get those guarantees right on the edges of your application.
[00:20:20]
And you don't have to do shotgun parsing or shotgun thing.
[00:20:25]
Shotgun parsing, yeah.
[00:20:26]
Yeah.
[00:20:27]
Yeah.
[00:20:28]
Yeah, that's a great point.
[00:20:30]
So it's something that is actually quite natural.
[00:20:34]
Actually another reason why I think it's very natural when you write Elm is because you
[00:20:39]
have to handle the error case.
[00:20:42]
Exactly.
[00:20:43]
Unlike even Haskell.
[00:20:44]
How so?
[00:20:45]
In Haskell, it's a warning if you have an inexhaustive case statement, right?
[00:20:52]
But it's not an error.
[00:20:54]
And Alexis talked about that even in the article.
[00:20:56]
One of her examples is doing list.head in Haskell and how list.head in Haskell is somewhat
[00:21:02]
broken.
[00:21:03]
By default, it actually returns an element rather than a maybe element.
[00:21:08]
Oh, yeah, the default, the standards head.
[00:21:14]
Yeah.
[00:21:15]
Exactly.
[00:21:16]
So Elm is unique in how built into the language that approaches.
[00:21:25]
Let's think of a few other examples because it's actually, I think it's quite widespread.
[00:21:30]
And I think it's nice to sort of connect these examples that a lot of people will be familiar
[00:21:37]
with this sort of concept of parse don't validate.
[00:21:41]
So one that comes to mind for me is remote data.
[00:21:45]
And I like the way you were framing this.
[00:21:48]
What would it look like if we didn't use this parse don't validate approach with JSON decoding
[00:21:53]
in Elm?
[00:21:54]
Well, what would it look like if we didn't use this parse don't validate approach with
[00:21:59]
remote data?
[00:22:01]
And the answer is, well, you could have a loading flag and you could have some data
[00:22:07]
that you check if it's loading.
[00:22:09]
Remote data gives you this nice type that says you're going to need to destructure this
[00:22:15]
code to see which value it is instead of just checking a Boolean to see if it's loading
[00:22:20]
or if there was an error, for example.
[00:22:23]
And I think another thing that the remote data example illustrates well is this concept
[00:22:30]
of dealing with uncertainty at the periphery of your application.
[00:22:34]
So with remote data, and I think maybe some people even use remote data and aren't familiar
[00:22:39]
with this sort of technique.
[00:22:41]
So you can just pass remote data down and check it.
[00:22:46]
But what works the nicest is when if you say, I have these five different remote data values
[00:22:51]
that I depend on, then you can say remote data dot map five.
[00:22:57]
And if any of them have an error, then you want to show an error.
[00:23:02]
And if some of them are loading, you want to show a loading screen instead of showing
[00:23:05]
five different loading spinners like you sometimes see online.
[00:23:11]
Which is reasonable also.
[00:23:14]
That's right.
[00:23:15]
It may be what you want in some cases.
[00:23:17]
That's absolutely right.
[00:23:18]
But often you want to take a set of things and maybe you even depend on these different
[00:23:25]
pieces of data.
[00:23:26]
So you can't do anything without this sort of aggregate piece of data, even though it
[00:23:29]
comes from five different HTTP requests.
[00:23:32]
And you just do remote data dot map five.
[00:23:35]
And now you've got these five different remote data types that all come together at once
[00:23:41]
rather than having to check all these different conditions.
[00:23:45]
And so you handle that uncertainty at the very top level.
[00:23:48]
And then you just have a code that's blissfully ignorant to the possible failures at the periphery.
[00:23:54]
And it can just confidently use all of that data.
[00:23:57]
It's got all of it.
[00:23:58]
It doesn't need to check.
[00:23:59]
Oh, well, if I have this and I don't have this, then handle it this way.
[00:24:04]
Yeah.
[00:24:05]
Yeah, this is usually what I use.
[00:24:07]
Maybe map two and all the other map and functions.
[00:24:13]
Same pattern.
[00:24:14]
And it's something that you see quite often.
[00:24:15]
Yes.
[00:24:16]
I saw one example in some code I was working with recently where I did a refactoring and
[00:24:22]
it turned out quite nicely.
[00:24:24]
What I was seeing was exactly this validation smell rather than parsing that we were checking
[00:24:31]
for a certain condition repeatedly and weren't able to use a data type to say, I need this
[00:24:38]
condition.
[00:24:39]
I wanted to strengthen the guarantees of the input I was getting.
[00:24:43]
So the example was there was this data type representing a document type and one of the
[00:24:48]
document types was unknown.
[00:24:51]
And that was just in this big custom type, document type is this type or that type or
[00:24:58]
that type or unknown.
[00:25:00]
And so you get code like if document type dot is unknown or if document type equals
[00:25:06]
equals document type dot unknown.
[00:25:10]
This is annoying to check for all the other ones.
[00:25:14]
Because there's special handling if it's unknown.
[00:25:17]
So I think this is like maybe sort of its own sub pattern that sometimes you have data
[00:25:22]
that's like mixed in and you want to be able to, again, you know, like we've been discussing,
[00:25:28]
you want to keep track of what you've proven.
[00:25:32]
So it's like if you've proven that you have a document type that's not unknown, now that
[00:25:37]
unknown type is just one of the variants of this big document type.
[00:25:42]
So to prove that it's not unknown, like there's no data type that can tell you that constraint.
[00:25:49]
So one thing you could do is you could, you know, you could have like a known document
[00:25:56]
type thing.
[00:25:57]
But then if you can have unknown in there, then it's like you could have a custom type
[00:26:02]
that says it's a known document type.
[00:26:05]
But if you have a document type type in there, that could include unknown.
[00:26:09]
So now you're possibly representing an impossible state.
[00:26:13]
So what I did instead is I created a new top level document type that was just document
[00:26:20]
type and document type was either unknown or a known document type.
[00:26:26]
Yeah.
[00:26:27]
And I created a separate type called known document type.
[00:26:29]
And I just took all of the variants except for unknown and put it into there.
[00:26:34]
And so now you can assert in certain functions that you have a known document type, in which
[00:26:39]
case you don't need to check if document type is unknown, then handle it this special way.
[00:26:45]
If you have known document type as the type of your function, you don't have to think
[00:26:49]
about that case.
[00:26:50]
Yeah, you've checked it once and you don't care about it afterwards.
[00:26:54]
Yeah, exactly.
[00:26:55]
And I think that this pattern comes up.
[00:26:59]
Maybe you have like, like I find this is quite common that you'll have different custom type
[00:27:04]
variants that have bits of data, you know, and if you know that you have these bits of
[00:27:10]
data, like maybe you have like a guest user has certain bits of data and a logged in user
[00:27:15]
has certain bits of data and admin user has certain bits of data.
[00:27:19]
But if you want to track that, if you've checked and the only users that are allowed to see
[00:27:24]
this page are admin users, if you have like a user custom type with those three different
[00:27:30]
variants, admin, logged in and guest, you can't use those variants to say the view function
[00:27:38]
for this page requires an admin user because it's a variant.
[00:27:41]
It's not a type.
[00:27:45]
So I think there's sort of like a, I don't know, maybe this is like an Elm refactoring
[00:27:49]
technique to sort of take the bits of data of like admin user and then just extract out,
[00:27:56]
you know, so if you have like admin user and it has either a record with bits of data or
[00:28:02]
positional values in the constructor, I think that's like a nice refactoring to like pull
[00:28:08]
out those bits of data, whether it's a record or positional constructor values, and then
[00:28:14]
create a new custom type called admin user.
[00:28:17]
And now you have type user equals admin user, admin user.
[00:28:23]
It's kind of weird.
[00:28:24]
But yeah, exactly.
[00:28:28]
Maybe you have some sort of clever naming convention that makes it less awkward there,
[00:28:34]
but that's a very common smell that I find that you lose track of which variant you have
[00:28:40]
and you want to strengthen your input assumptions and you can't do that because it's locked
[00:28:45]
in this variant, which you can't check for an Elm.
[00:28:49]
You don't have to extract the admin data at the top level where you defined the user.
[00:28:55]
You can just create a new admin user data or whatever better name you find locally at
[00:29:04]
the location where you need it.
[00:29:06]
So you don't need to refactor your whole application just because this one case.
[00:29:11]
Right.
[00:29:12]
That's a good point.
[00:29:13]
But if it makes sense in other places, then yeah, by all means, please do.
[00:29:18]
Right.
[00:29:19]
And I think that's a very good point.
[00:29:21]
And I think maybe it brings up another question, which is how do you control the creation and
[00:29:29]
the sort of destructuring and reading of that data?
[00:29:33]
So some data you want to protect, we discussed this in our opaque types episode.
[00:29:38]
So it might be like a social security number that you want to be careful about how it's
[00:29:43]
presented and make sure it's encrypted before it's sent over HTTP or whatever.
[00:29:48]
But you also want to protect sort of low level data, right?
[00:29:52]
Well, I guess another way to handle protecting low level data is to not make it low level,
[00:29:57]
to wrap it in a type that represents the assumption.
[00:30:00]
So I guess it depends on where you want that abstraction to be.
[00:30:04]
For example, if you have an ID that's just an int or maybe you have like an admin user
[00:30:10]
ID type or a user ID type, you can have more low level data if it's encapsulated in its
[00:30:19]
own module that's protected, that you can't directly read the data from.
[00:30:23]
You can only get it in the functions that are exposed by the admin user module.
[00:30:27]
So there's the question of do you have access to internal data, which may be too low level
[00:30:32]
to use?
[00:30:34]
And then do you have access to create that data?
[00:30:37]
So if you can just create an admin user, what's to stop you from taking the regular user data
[00:30:43]
you have and you're like, I just want to see if this admin user page works.
[00:30:47]
So I'm going to create an admin user from the current logged in user because I don't
[00:30:51]
want to handle those other cases now.
[00:30:53]
And so you take those bits of data and then you pass in an empty string for this one thing
[00:30:58]
you need and a, you know,
[00:31:01]
Beautiful.
[00:31:02]
So you still want to make impossible states impossible and you still want to use this
[00:31:09]
sort of gatekeeper approach of protecting how you create and consume data.
[00:31:15]
Yeah.
[00:31:16]
So you would basically have a parser function to create your admin.
[00:31:21]
Exactly.
[00:31:22]
In a way.
[00:31:23]
That's right.
[00:31:24]
Yeah.
[00:31:25]
So how would you go about this?
[00:31:27]
You have a condition that you want to check.
[00:31:29]
So you want to branch off based on some condition, but there's no way to extract information
[00:31:37]
that is of a different type than what you have in the other condition.
[00:31:44]
You would have a name, for instance, which is a string containing the first name and
[00:31:49]
the last name of type string in one condition and the other condition you would just have
[00:31:54]
the first name.
[00:31:55]
So in both cases, you would have a string formatted just like you want to, but they're
[00:32:01]
the same types.
[00:32:03]
How would you go about that?
[00:32:04]
My guiding principle for cases like that is always wrap early, unwrap late.
[00:32:10]
You know, we talked about that, I think, in the opaque types episode.
[00:32:14]
And I'm always thinking like you want to have, I think Alexis talks about this in her article
[00:32:20]
as well.
[00:32:21]
You want to have the most pure representation of the data.
[00:32:25]
She talks about this approach of like letting the data types drive the implementation, not
[00:32:30]
having the implementation drive the data types that you define.
[00:32:33]
So think about the data types up front.
[00:32:36]
That's sort of like a data modeling technique and mindset is that you think about your data
[00:32:43]
types and think about your ideal data types, continually sort of refine and refactor your
[00:32:49]
data types to make them express your constraints better.
[00:32:53]
So at the core of your application, you want to have nice data that clearly represents
[00:32:58]
the semantics that clearly represents the constraints and guarantees, the things that
[00:33:03]
you've proven.
[00:33:04]
And as you get more and more into the core of your application, you should have more
[00:33:08]
and more refined data types and assumptions.
[00:33:11]
You know, so you, you should have fewer maybes, you should have, you know, fewer built in
[00:33:17]
types, and you should have more custom types and more things that illustrate these guarantees.
[00:33:22]
Um, more non empty lists and things like that.
[00:33:26]
So that's my sort of guiding principle is, you know, wrap early.
[00:33:29]
So I want to avoid this sort of shotgun parsing approach where I'm mixing validation logic
[00:33:35]
with processing logic.
[00:33:37]
I want to validate and parse into nice data types that prove my assumptions at the periphery
[00:33:45]
as soon as I possibly can.
[00:33:48]
Ideally, I don't even want it to exist as a string, because my decoder immediately turns
[00:33:54]
it into this value or fails if it can't.
[00:33:57]
If I expect the username to be a non empty string, or to be of a certain format, or to
[00:34:04]
not include certain characters, then I want my decoder to actually do that.
[00:34:09]
So I never have it as a string, it will just fail if it doesn't meet those conditions,
[00:34:14]
or I'll have I'll have it parsed into my valid username type.
[00:34:18]
And then sometimes it's tempting, we kind of talked about this example before that you
[00:34:22]
could like, you could do maybe dot with default empty string, and then pass around this empty
[00:34:28]
string that maybe it represents all these different things.
[00:34:31]
That's a violation of this concept of wrap early unwrap late.
[00:34:35]
So you wrap early, you have your ideal form of the data, the nicest data type.
[00:34:40]
And then at the last possible second, you're forced to have a string because or you know,
[00:34:46]
a JSON encode value or whatever, you've lost information and semantics, but you have to
[00:34:51]
because that's what that API requires.
[00:34:54]
So at the last possible second, you unwrap that data into these lossier data formats.
[00:35:01]
And that all happens at the periphery of the app.
[00:35:03]
Yeah, you lose information as soon as you don't need them anymore.
[00:35:07]
Also,
[00:35:08]
yes, exactly. So all along the way, there's not an opportunity for mistakenly using that
[00:35:15]
data in a way that the guarantees aren't enforced, because your data types help you enforce the
[00:35:21]
guarantees.
[00:35:22]
You don't lose semantic information because the types help enforce the semantics.
[00:35:26]
So yeah, I think wrap early unwrap late would be my sort of core guiding principle there.
[00:35:32]
It's yeah, definitely.
[00:35:33]
Yeah, it takes practice to find how to do that.
[00:35:35]
But I think it's always a good idea.
[00:35:38]
What I'm thinking right now is that if you have the same data in both conditions, the
[00:35:43]
same primitives, and the same data that it really contains, one way that you could keep
[00:35:50]
using this technique is to use phantom types.
[00:35:53]
Yes.
[00:35:54]
So phantom types are a way to differentiate your types, even though their contained data
[00:36:01]
is the exact same.
[00:36:03]
Yes, exactly.
[00:36:04]
So you would have a parsing function that returns a data type with a type variable,
[00:36:09]
which is a phantom type.
[00:36:11]
So you would have, you could match them, you could call function that requires that the
[00:36:17]
phantom type is in a certain state, is of a certain type, but you would still have the
[00:36:23]
same data, you wouldn't need to create a new custom type.
[00:36:26]
Yes.
[00:36:27]
So both techniques are nice, yet the situation will tell which one is best.
[00:36:31]
Yes, and that's actually exactly what Richard Feldman's Elm validate package does.
[00:36:37]
It runs it through a series of validations, and then it has a sort of phantom type that
[00:36:43]
it uses to keep track of the fact that you validated those conditions.
[00:36:48]
So that's definitely a valid approach, if you will.
[00:36:55]
We'll have to check that.
[00:36:58]
You'll have to parse that.
[00:36:59]
That sounds very weird.
[00:37:01]
So you just opened the bank accounts.
[00:37:05]
We're going to have to parse your information.
[00:37:07]
Sorry, what?
[00:37:11]
Don't validate my information, parse it.
[00:37:14]
This is unprofessional.
[00:37:17]
Yeah, I think phantom types definitely fit in here.
[00:37:22]
It is interesting because you can unwrap the phantom types in a more permissive way, depending
[00:37:28]
on how you structure it.
[00:37:29]
So for example, let's say you have an ID type, and it has a phantom type with different ID
[00:37:36]
types.
[00:37:37]
You can just unwrap the ID.
[00:37:39]
You don't have to use specific ways to unwrap it.
[00:37:43]
It all requires human judgment to think about what guarantees do I want to provide, and
[00:37:48]
how do these different Elm devices that I'm using help me do that?
[00:37:54]
You have phantom types that you use to construct things.
[00:37:59]
Another way of saying it, there are types for which you can only build a type with a
[00:38:06]
certain phantom type using some functions.
[00:38:11]
You can make sure that there's only one way to build a value with this exact type, with
[00:38:18]
this exact value for a phantom type, and that is using this function.
[00:38:23]
This function is your parser function, basically.
[00:38:27]
Then you can probably destructure it using functions.
[00:38:34]
This is only true if you're using an opaque type.
[00:38:38]
Yes.
[00:38:39]
Right.
[00:38:40]
Like for the phantom thing that you use, you can expose the type of your phantom type,
[00:38:46]
but not the constructor for the phantom type.
[00:38:48]
So the only thing that can build values with that phantom type attached, user ID for example,
[00:38:55]
can only be built in this one module because the constructor is hidden in that module.
[00:39:00]
Yeah.
[00:39:01]
So basically the idea is to get you across that border.
[00:39:05]
So you need to enter this function.
[00:39:08]
You need an ID with this type for a phantom type.
[00:39:14]
And once that is done, you don't care about the underlying data or the phantom type value
[00:39:19]
anymore.
[00:39:20]
Right.
[00:39:21]
You just want to access the value of the ID.
[00:39:24]
Yes.
[00:39:25]
I don't remember if I brought up this analogy in our opaque types episode or not.
[00:39:29]
Sometimes I think about those wristbands that they give you.
[00:39:33]
If you go to a beer garden and they give you a wristband that validates that you're of
[00:39:39]
drinking age, or you go to a concert and they give you a wristband that says that you paid
[00:39:44]
for your ticket.
[00:39:45]
Once you're in the concert venue, people don't have to keep checking your ticket.
[00:39:49]
Or once you're at the festival, you don't have to show your ID to buy a drink because
[00:39:56]
that's already been parsed, if you will.
[00:39:59]
Exactly.
[00:40:00]
They don't have to validate it at every stand.
[00:40:02]
There's absolutely no way that people will exchange their wristbands though.
[00:40:07]
So it's all totally safe.
[00:40:09]
That's right.
[00:40:10]
Exactly.
[00:40:11]
You have to think about that.
[00:40:12]
You have to be thinking like a security person.
[00:40:16]
In this case, I think you want to unwrap a bit later.
[00:40:22]
I think the constructors are too exposed in this case.
[00:40:26]
I think you're right.
[00:40:27]
I think you're right about that.
[00:40:29]
Another thing about phantom types versus custom types is you can potentially grab the bits
[00:40:36]
of raw data and it doesn't force you to handle the bits of data.
[00:40:41]
You can unwrap them.
[00:40:43]
Again, it's just a matter of using your own judgment to think about what are the possible
[00:40:49]
ways that these assumptions that I'm trying to provide can be bypassed.
[00:40:55]
That's the question.
[00:40:56]
Yeah.
[00:40:57]
Basically, if you use this technique well, you notice it because one, you don't have
[00:41:01]
to fake conditions or fake branches.
[00:41:05]
So basically, you made impossible code paths impossible.
[00:41:09]
Things that you can't test, those are impossible.
[00:41:13]
And two, you have not faked any data anywhere in the code.
[00:41:19]
Yes.
[00:41:20]
Right.
[00:41:21]
By passing in like maybe with default value.
[00:41:24]
Exactly.
[00:41:25]
Yes, that's a great point.
[00:41:27]
That's a major red flag.
[00:41:28]
If you are just in an impossible case where it's like, this should never happen, instead
[00:41:34]
of throwing an exception in a language other than Elm, you do a similar thing where you
[00:41:39]
just say, I'll just return this value because this will never happen.
[00:41:44]
So I'll just return the string.
[00:41:47]
This should never happen.
[00:41:48]
Or worse, you call the function recursively so that the Elm compiler is happy.
[00:41:55]
Exactly.
[00:41:56]
Those are code smells.
[00:41:57]
Yeah, that's the code smell.
[00:41:59]
If you arrive at this should never happen, it should be in your parsing code at the periphery,
[00:42:06]
the code that's doing the initial logic to make sure the input is good.
[00:42:11]
And then you just immediately exit.
[00:42:14]
Otherwise, you have the good data and you run your core logic.
[00:42:17]
Yeah.
[00:42:18]
If you do get into that will not happen case, at the very least, leave a comment.
[00:42:25]
At the very, very, very least.
[00:42:29]
Because otherwise, people will not be happy when they discover that this is weird.
[00:42:35]
Yeah.
[00:42:37]
Or use a co worker's computer to do the commit so that the get blame show somebody else's
[00:42:42]
name.
[00:42:43]
Yeah.
[00:42:44]
Let's talk a little bit about how you would, you know, you're going through your code base
[00:42:47]
and you're looking for opportunities to improve your code using this parse don't validate
[00:42:52]
idea.
[00:42:53]
What should you be on the lookout for?
[00:42:55]
Look for those comments.
[00:42:56]
Look for this should never happen code.
[00:42:59]
What else should you be on the lookout for?
[00:43:03]
Then you notice that a certain condition is made several times.
[00:43:06]
Yeah.
[00:43:07]
So if this is empty, and in that, that branch, you see, if this is empty again, which is
[00:43:15]
not often the case, usually you will have that in separate functions, which is, which
[00:43:20]
makes it a bit harder to tell.
[00:43:22]
I sometimes see functions like is thing like this is just Yeah, it is just Yeah.
[00:43:32]
So if you return a Boolean, that is usually not great that people refer to that as Boolean
[00:43:39]
blindness sometimes.
[00:43:40]
But sometimes, you also see that when you have a function returns like a tuple, the
[00:43:46]
first one being a Boolean, and the second element being the value that you are using.
[00:43:53]
So that is kind of parsing don't validate, but you're still returning a Boolean.
[00:43:57]
You're not keeping track of it in a very safe way.
[00:44:01]
Yeah, exactly.
[00:44:03]
So that is the one that I've seen most and which is the most actionable for me.
[00:44:10]
Like that is an instant red flag.
[00:44:12]
And that is not very hard to tell.
[00:44:14]
But it's quite rare.
[00:44:16]
Right.
[00:44:17]
Yeah.
[00:44:18]
And then also, like, you know, this, this other case, you were mentioning where you're
[00:44:22]
essentially, I think of it that you're discarding information you'd get from a case statement
[00:44:29]
destructure.
[00:44:31]
So like if you're saying is just Yeah, is that always a bad thing?
[00:44:36]
No, maybe there's some cases where it's okay, but be on the lookout for it.
[00:44:40]
Like, I would look very carefully at code that's checking is just and then doing something.
[00:44:46]
And if it does is just followed by maybe with default, yeah, or using that value anyway,
[00:44:52]
exactly in any way.
[00:44:53]
Yeah, exactly.
[00:44:54]
So if it destructures a value, and then uses that value, that's a clear smell.
[00:45:01]
Yes.
[00:45:02]
So one thing that I that you will see a lot when you try to do to apply this technique,
[00:45:08]
I think is you have a bunch of functions, which work with a maybe string, for instance.
[00:45:16]
And in one case, you know that is a string, what would you think should you then just
[00:45:21]
wrap it in and just or should you duplicate the function which takes a maybe string, but
[00:45:25]
then doesn't handle the nothing case, you're talking about like destructuring and then
[00:45:31]
piecing it back together the way it was in a way but not piecing it back into the same
[00:45:37]
value just you have a username.
[00:45:40]
So you have a function which takes that user, maybe username and displays it.
[00:45:46]
And in some parts of the code, you know that a username is just Yes, we still want to display
[00:45:53]
it from the username.
[00:45:55]
So you call that.
[00:45:58]
So you're putting a just in front of it because you're calling a function that takes a maybe
[00:46:03]
string and shows the username?
[00:46:04]
Yeah, right.
[00:46:05]
That's a good question.
[00:46:07]
I definitely have seen code like that.
[00:46:09]
Oh, yeah, me too.
[00:46:11]
It does generally feel like a smell.
[00:46:14]
I'm not going to go so far as to say that it's always a bad thing.
[00:46:17]
But I would definitely look very suspiciously at that code.
[00:46:21]
Yeah, yeah, I think I think you're right.
[00:46:23]
If you're like, I mean, Alexis kind of says this in the article, I think that you should
[00:46:28]
always be progressing towards more structured data.
[00:46:32]
And you should always be proving more information.
[00:46:36]
But adding it adjust is sort of reducing the amount of information you have.
[00:46:40]
So that so that would be no, you're adding chaos.
[00:46:43]
No.
[00:46:44]
Yeah.
[00:46:45]
Entropy.
[00:46:46]
Yeah, I don't know.
[00:46:47]
You always want to.
[00:46:48]
Yeah, unlike the universe, your code should always be decreasing the entropy as it goes
[00:46:52]
along.
[00:46:53]
Yeah, I think so.
[00:46:56]
Maybe in that case, you could look at all the call sites and check, oh, maybe they're
[00:47:02]
all just like all calls to this function is just so you can simplify it.
[00:47:07]
Yeah, maybe not.
[00:47:09]
Maybe not.
[00:47:10]
Good point.
[00:47:11]
But in the case where it's just in the function you were calling with wrapping it in adjust,
[00:47:18]
maybe the just case has a function that could be extracted out of that just clause.
[00:47:22]
Yeah, can be shared.
[00:47:23]
So you call it in the just case.
[00:47:25]
But maybe you can directly call that function and expose that as well.
[00:47:29]
Yeah, that sounds good.
[00:47:32]
Sometimes it's a nice pattern.
[00:47:33]
Like sometimes I see sort of core business logic code.
[00:47:39]
I think it's sort of an example of this shotgun parsing idea that you are checking for maybe
[00:47:45]
and doing maybe with default in your core logic.
[00:47:48]
But that should be the responsibility of the calling code.
[00:47:51]
So I think another thing is it's probably a smell if you have a lot of helper functions
[00:47:56]
that deal with maybes when if you think about the responsibilities, right, if you have like
[00:48:01]
a set of maybe helper functions, then of course, they're going to accept maybes because that's
[00:48:06]
the responsibility.
[00:48:07]
Yeah.
[00:48:08]
But if it's like presenting a username showing a guest username, and you say like, show username,
[00:48:14]
you know, and it takes a maybe, it's actually probably better to not take a maybe to take
[00:48:22]
a username, and it presents it however it does it.
[00:48:25]
And then you do maybe.with default, guest or whatever you're going to do.
[00:48:30]
But the calling code can take care of that uncertainty.
[00:48:33]
Otherwise you get into the I see that happen a lot where you're validating the code, where
[00:48:38]
you're processing it, right?
[00:48:40]
So this principle is really helpful that like, you should be just confidently asserting that
[00:48:45]
you have this code.
[00:48:47]
Don't hesitate and second guess yourself and say, if I have this maybe value, and then
[00:48:51]
I'll handle this nothing case this way, leave that to the caller.
[00:48:55]
In a lot of cases, that's better.
[00:48:57]
So another smell I think that you can pay attention to is and a lot of these things,
[00:49:03]
I think you want to let when the compiler is telling you something is wrong, when the
[00:49:07]
compiler seems to be making you do extra work, you can go two different directions, you can
[00:49:12]
go the direction of the compiler is telling me, I need to handle this case.
[00:49:18]
So I'll just make it happy and use with default or passing this hard coded value here in this
[00:49:26]
thing that will never happen, right?
[00:49:27]
That's telling you a smell.
[00:49:29]
So it's really about what do you do when the compiler is telling you, you're not handling
[00:49:34]
something.
[00:49:35]
That's one thing to pay attention to.
[00:49:36]
Another thing to pay attention to is when you're writing tests, are you testing things
[00:49:40]
that you shouldn't even be able to test?
[00:49:43]
Because if you wrote your types in the right way, where you guaranteed those assumptions
[00:49:50]
and you made impossible states impossible, you wouldn't even be able to write the test
[00:49:54]
because it wouldn't compile.
[00:49:55]
So I'd say another code smell is being able to write compiling tests that should be impossible
[00:50:03]
states.
[00:50:04]
Or in other words, have full coverage of function where you cannot have full coverage because
[00:50:11]
they're impossible states.
[00:50:13]
Yeah, exactly.
[00:50:15]
Exactly.
[00:50:16]
Exactly.
[00:50:17]
You know, another principle I think about a lot, you know, I mentioned this earlier,
[00:50:21]
if somebody new to your code base comes in, if they use something wrong, I would say the
[00:50:26]
onus is on the code.
[00:50:27]
It's something that should be fixed in the code, not, hey, we need to teach you how to
[00:50:32]
use this properly.
[00:50:33]
That's a smell.
[00:50:34]
Yeah, you shouldn't have to train your teammates after the fact that they've learned Elm.
[00:50:40]
Exactly.
[00:50:41]
Exactly.
[00:50:42]
So the training should be coming from the compiler telling them you can't do this.
[00:50:47]
And then they say, hey, why can't I do this?
[00:50:49]
Maybe you have to explain why the compiler isn't allowing it, but the compiler should
[00:50:52]
disallow it.
[00:50:53]
So I feel very uncomfortable if I have to keep track of constraints in the way I'm using
[00:50:59]
something.
[00:51:00]
As much as possible, and it's a, you know, you're not going to have it on day one on
[00:51:06]
the first iteration built to, you know, make all the assumptions possible.
[00:51:10]
And you'd probably be doing something wrong in your process if you were.
[00:51:15]
These things emerge over time.
[00:51:17]
But as you discover ways that it's used, bugs that are happening, you should be making impossible
[00:51:23]
states more impossible as you discover them happening.
[00:51:27]
That's part of the process of working with an Elm code base.
[00:51:30]
Oh, yeah.
[00:51:32]
So this is very simple and in a way, as I said before, quite natural technique for Elm
[00:51:38]
developers and less so for people come from JavaScript or Ruby.
[00:51:45]
I think we got it pretty well covered.
[00:51:47]
Yeah.
[00:51:48]
I hope this was useful to you, listener.
[00:51:50]
Yeah.
[00:51:51]
And if it's painful, you might want to try parsing, not validating.
[00:51:55]
OK, well Dillon, see you next time.
[00:52:01]
Been a pleasure as always.
[00:52:02]
See you next time you're in.